This is the third and final installment of the in-place editing sample series. In part 1, I demonstrated a how Atlas (server and client) can be used to implement in-place editing functionality. In part 2, I walked through the implementation of the client-side behavior that implements the UI logic and functionality in script neatly packaged in the form of reusable script components. In this part, I am going to demonstrate how you can write an extender control that enables you to provide a server-programming model to your client-side behavior.
I covered the client-side behavior first, because we are going to leverage it in our server control implementation (as you may have guessed). One of the key motivations behind the server control model is to enable application developers to build rich experiences without having to delve into script. Unfortunately there is no shortcut... doing interesting things on the client requires script, and someone needs to write it. In the context of this sample (and speaking generally as well), the component developer needs to write script (who we assume is more advanced) than every application developer, and the app developer can then simply use components to implement the actual application. The component developer can now use the Atlas script framework and all of its advanced concepts as an enabling technology, and a more disciplined scripting approach rather than throwing script out to the client in a relatively ad-hoc manner that works on top of raw JavaScript and DHTML.
<aside>
The extender control approach enables extending existing controls (for example <asp:TextBox>) rather than introducing new derived controls. Just like behaviors on the client, I think this is a pretty compelling model, because it helps one avoid building kitchen-sink style controls. The same TextBox control can be associated with more than one extender to provide new bits of independent functionality.
Extender controls build on extender provider concept present in the .NET framework. ASP.NET itself doesn't really support extender providers at runtime, so Atlas provides a framework that enables persistence of extender properties associated with controls, and a designer framework that does leverage the extender provider concept in order to expose the extender's (InPlaceEditExtender) properties in the property grid when the extendee (the TextBox) is selected. Check out this usage model from the first post in the series.
</aside>
Step 1: Defining the Extender Properties
The first step to implementing an extender control is defining a properties object deriving from TargetControlProperties that will allow the extender control to hold data for each extendee. Instances of this object are persisted into the markup, so the usual persistence-related metadata that apply to controls apply here as well. The base class implements IStateManager provides a ViewState dictionary just like the base Control class.
public class InPlaceEditExtenderProperties : TargetControlProperties {
[DefaultValue(false)]
public bool Enabled {
get {
object o = ViewState["Enabled"];
return (o != null) ? (bool)o : false;
}
set {
ViewState["Enabled"] = value;
}
}
protected override bool IsEmpty {
get {
return (Enabled == false) &&
(LabelCssClass.Length == 0) &&
(LabelHoverCssClass.Length == 0);
}
}
[DefaultValue("")]
public string LabelCssClass {
get {
string s = (string)ViewState["LabelCssClass"];
return (s != null) ? s : String.Empty;
}
set {
ViewState["LabelCssClass"] = value;
}
}
...
}
LabelCssClass is one of the properties of the extender. Typically, Enabled is a property of any extender. The extender applies to all controls on the page in the same naming container (in this case all TextBoxes). Hence you need an Enabled boolean property to track the specific textboxes that are being extended. In addition, you must implement the IsEmpty property, which the designer uses to ensure only non-empty extender property objects are persisted into the markup.
Step 2: Implement the Extender Control
The next step is to make use of this properties object in the extender implementation.
public class InPlaceEditExtender : ExtenderControl<InPlaceEditExtenderProperties> {
private static readonly ScriptNamespace nStuffNamespace = new ScriptNamespace("nk", "nk");
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
if (!DesignMode) {
ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
scriptManager.RegisterScriptNamespace(nStuffNamespace);
}
}
protected override void RenderScript(ScriptTextWriter writer, Control targetControl) {
InPlaceEditExtenderProperties properties = GetTargetProperties(targetControl);
if ((properties == null) || (properties.Enabled == false)) {
return;
}
string labelCssClass = properties.LabelCssClass;
...
writer.WriteStartElement("nk:inPlaceEdit");
if (labelCssClass.Length != 0) {
writer.WriteAttributeString("labelCssClass", labelCssClass);
}
...
writer.WriteEndElement();
}
}
Atlas-enabled controls typically render out XML-script that is used a mechanism to define and customize and wire together script component instances. Hence RenderScript is the most interesting method in the implementation, where the specified ScriptTextWriter is used to render out XML. The implementation retrieves the properties object holding data for the specified control, and renders out XML tags as appropriate.
For reference, here is the snippet of XML-script, where the <control> tag is rendered by the extendee, and the <nk:inPlaceEdit> tag is rendered by our extender control.
<control id="nameTextBox">
<behaviors>
<nk:inPlaceEdit
labelCssClass="inPlaceEditLabel" labelHoverCssClass="inPlaceEditLabelHover" />
</behaviors>
</control>
The interesting question is how the extender gets to emit script within the context of the extendee. The ExtenderControl base class does a few things in OnPreRender that enable this. First, it locates the ScriptManager, and calls RegisterControl on each of its extendee controls. This ensures an XML-script tag corresponding to the extendee control is rendered (even if the extendee is not an Atlas-enabled control itself, i.e., doesn't implement IScriptControl in which case a generic wrapper IScriptControl is created). ScriptManager returns the IScriptControl implementation corresponding to the registered control. The ExtenderControl base class then registers itself as a behavior by calling RegisterBehavior on the resulting IScriptControl, passing in its implementation of IScriptBehavior. When the extendee control renders out its XML-script (by virtue of being registered with the ScriptManager), it calls into the extender's RenderScript method defined on IScriptBehavior.
In Atlas components are implemented in namespaces to avoid conflicts. XML-script also has the notion of namespaces. In order to have your namespace registered as an xmlns in the resulting XML-script, this control calls RegisterScriptNamespace on the ScriptManager in its OnPreRender implementation.
Step 3: Implement the Designer
The designer is where the .NET extender provider model is utilized. Essentially the designer specifies the range of controls that it can extend, and the properties to be exposed on the extendee.
[ProvideProperty("InPlaceEditing", typeof(TextBox))]
public class InPlaceEditExtenderDesigner : ExtenderControlDesigner<InPlaceEditExtenderProperties> {
protected override bool CanExtend(Control targetControl) {
return (targetControl is TextBox);
}
protected override InPlaceEditExtenderProperties CreateProperties(Control targetControl) {
return new InPlaceEditExtenderProperties();
}
[
Category("Behavior"),
TypeConverter(typeof(TargetControlPropertiesConverter))
]
public InPlaceEditExtenderProperties GetInPlaceEditing(TextBox textBox) {
return GetTargetProperties(textBox, /* createIfRequired */ true);
}
}
Like its runtime counterpart, ExtenderControlDesigner is a generic class. InPlaceEditExtenderDesigner offers the "InPlaceEditing" property to all TextBoxes. This is specified by the presence of the ProvideProperty attribute on the class, and the implementation of the corresponding property getter. The getter itself is provided the specific textbox being extended, so the associated properties object can be handed out. The designer overrides CanExtend so it can indicate the specific type of controls it can extend. By default all controls in the same naming container can be extended, so you only need to override it if you are constrained to a specific type of control.
That's pretty much it. A lot of the internal plumbing is handled by the Atlas framework. For example, automatic management of property object instances when extendee controls are re-ID'd or deleted, etc.
Hopefully this series has shown a glimpse of the extensibility in the platform, so I look forward to seeing some custom component and control development :-)