InPlaceEditing - Implementing Extender Server Controls

The third and final part of this series covers the implementation of the InPlaceEditExtender server control that allows ASP.NET page developers to easily incorporate in-place editing functionality without having to delve into client-side programming.

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 :-)

Posted on Saturday, 2/18/2006 @ 3:44 AM | #ASP.NET


Comments

18 comments have been posted.

Rob Howard

Posted on 2/18/2006 @ 4:24 PM
Did you see the inplace Ajax editing we did with Community Server?

It's very, very cool stuff :)
http://communityserver.org/blogs/videos/archive/2006/01/31/512538.aspx

Steve

Posted on 2/19/2006 @ 6:17 AM
A bit off topic here, but if I create a server control that uses Atlas - I assume it requires the ScriptManager.

If a user then adds that control to their application and the page already has a ScriptManager - what would happen?

Is it ok practice to do this?

I was going to create a composite control with Atlas behaviors

Ayman Farouk

Posted on 2/19/2006 @ 1:46 PM
Great post. But we still need some Java scripting in order to create our own Atlas Server controls, dont we !?
A.

Steve

Posted on 2/19/2006 @ 2:14 PM
I would interpret this "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" to mean yes.

I personally am looking for the asp.net team to help making creating the javascript easier - ie. more javascript/debugging tools within VS 2005 for example and providing a good library that will help promote cross browser functionality.

It's alot to ask for, but it gives some vision to help making asp.net stay on the cutting edge of rich internet applications, which I think is only going to continue to grow and expand.

I enjoyed the video that Nikhil made showing the spectrum on the whiteboard.

Andre

Posted on 2/20/2006 @ 3:24 PM
First of all, great job on Atlas so far.
Sorry for being off topic here a little, but I have to share a few thoughts/problems I have encountered so far

1) The caching example (http://atlas.asp.net/quickstart/atlas/doc/services/default.aspx#caching) does not seem to work, I have tried to use it on on of my pages and it did not work either (I'm using the Jan release)

2) It would be nice to get some examples how to use Atlas with custom controls (especially where to define the ScriptManager when you have more than one control on a page). Is there a way to register the ScriptManager with the Page from within the custom control?

I tried to add this to the page load event of my control:
Dim MyScriptManager As New Microsoft.Web.UI.ScriptManager
MyScriptManager.EnablePartialRendering = True
Page.Controls.Add(MyScriptManager)

but then I will get the following error: The control collection cannot be modified during DataBind, Init, Load, PreRender or Unload phases.
Of course, even if this was working, I still would not know how to solve the problem of having multiple Atlas-driven controls on a page.

Nikhil Kothari

Posted on 2/20/2006 @ 9:40 PM
Ayman: As mentioned, someone needs to write the script - client-side functionality doesn't just come for free. The idea is to have a better scripting platform, and better mechanism to encapsulate functionality so it can be reused by app developers without writing that script.

Steve: Better scripting platform does include better tools. We plan to enhance the offering in the orcas tools timeframe.

Only a single ScriptManager is allowed. It must be in the page before any atlas-enabled controls. It cannot be added dynamically, other that at page construction time (which is the generated code corresponding to the .aspx). You can use a ScriptManager in a master page. If you need to specify script manager settings at the content page level, you can use the ScriptManagerProxy control on the content pages. Custom controls that are Atlas-enabled should look for a ScriptManager, and raise an error message if it is absent.

Jivi

Posted on 2/21/2006 @ 4:27 AM
Rob the Community Server in place editing works only in IE, where as with Atlas is both Firefox and IE Friendly.

Thanks

Holan Gallé

Posted on 2/28/2006 @ 1:59 AM
Hallo!

Thanks for this great work.
We are using a complex asp.net 1.0 apps inhouse.
We are testing now asp.net 2.0 and atlas for our next release.

We have one BIG question.

What is yor roadmap for atlas???
We could not find details about this here on other atlas-related websites from MS.
This would be very important to us. When could we use a final-release.
The current release will not allow us to use this in production enviroment.
We must make a time-plan.
PLEASE ANSWER!

THANK YOU

flr

Posted on 3/1/2006 @ 12:55 AM
I do auto test codes about web application these days , so I want to know something about mshtml. I know this dll is a big stuff. I want to know what I should do for understanding this dll easily and clearly... thx : )

Korez Santana

Posted on 3/22/2006 @ 7:17 PM
Hi, great work really. I am facing some problems in web.config to define the appropriate namespace and assembly tags. I am always getting:

Assertion Failed: Unrecognized tag nk:inPlaceEdit

any idea on how to fix this?

Runi Thomsen

Posted on 3/24/2006 @ 10:43 AM
I realized that you will have to change the two bottom lines in InPlaceEdit.js in order for it to not generate errors in the marts CTP.

Change them the following:
Type.registerSealedClass('nStuff.Samples.InPlaceEdit.InPlaceEditBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('nk', 'inPlaceEdit', nStuff.Samples.InPlaceEdit.InPlaceEditBehavior);

And you will get no errors.

This is nessesary, because of a namespace change in marts CTP.

However, after switching to marts CTP, the InPlaceEdit control seams to have no effect at all. Is there a fix ?

Kori Francis

Posted on 3/24/2006 @ 12:26 PM
Very good work Nikhil .. I'm really enjoying developing with Atlas.

I think you should throw up a sample of how to create a control based on an existing extender. Like, what I specifically like to see would be a CustomDragOverlayExtender .. because I'm trying to write one now, but running into all kinds of problems by adding new properties. Javascript errors like _activeDragVisual is null or not an object or _activeDragSource is null or not an object.
Help help!

tal avissar

Posted on 3/30/2006 @ 2:50 PM
I realized that you will have to change the two bottom lines in InPlaceEdit.js in order for it to not generate errors in the marts CTP.

Change them the following:
Type.registerSealedClass('nStuff.Samples.InPlaceEdit.InPlaceEditBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('nk', 'inPlaceEdit', nStuff.Samples.InPlaceEdit.InPlaceEditBehavior);

And you will get no errors.

This is nessesary, because of a namespace change in marts CTP.

However, after switching to marts CTP, the InPlaceEdit control seams to have no effect at all !!! Is there a fix ?

What is the answer?

John

Posted on 4/3/2006 @ 8:57 PM
Regarding tal's issue, I am having same problem with March CTP with an extender I created using techniques to create InPlaceEdit with December CTP

Werner

Posted on 4/4/2006 @ 1:03 AM
Hi For this to work on the march ctp you need to change the bottom line from:
Type.registerSealedClass('nStuff.Samples.InPlaceEdit.InPlaceEditBehavior', Sys.UI.Behavior);
to:
nStuff.Samples.InPlaceEdit.InPlaceEditBehavior'.registerSealedClass('nStuff.Samples.InPlaceEdit.InPlaceEditBehavior', Sys.UI.Behavior);

Jaime

Posted on 4/4/2006 @ 6:24 AM
Hi nikhilk,

This post is one of the most useful things I read about Atlas. My congratulations for this job.

Well, I tried to create a new component following your steps, but it didn't work. Then, I tried your example that I downloaded and....didn't work too. I'm using the Atlas release of March. Do you know if this code works for the new release???

PD: Sorry, but I've forgot to say that I changed all the references of Web for Sys in the client code script. If you don't make this change the explorer returns an error.

Pack27

Posted on 5/16/2006 @ 8:10 AM
Hi For this to work on the march ctp with IE you need to :

1 . change the bottom line from:
Type.registerSealedClass('nStuff.Samples.InPlaceEdit.InPlaceEditBehavior', Sys.UI.Behavior);
to:
nStuff.Samples.InPlaceEdit.InPlaceEditBehavior.registerSealedClass('nStuff.Samples.InPlaceEdit.InPlaceEditBehavior', Sys.UI.Behavior);

2. Change All "Web." to "Sys."

3. Change :

if (Web.Application.get_type() == Web.ApplicationType.InternetExplorer) {
_labelElement.attachEvent('onfocus', _labelFocusHandler);
}
else {
_labelElement.attachEvent('onclick', _labelFocusHandler);
}

to

_labelElement.attachEvent('onfocus', _labelFocusHandler);

Matthew Becker

Posted on 6/15/2006 @ 10:22 AM
Hey, I was curious if anyone had an idea how to save the information after they finish each text box. For instance, once the textbox loses focus. I'm not quite sure how that would be accomplished. Thanks!
The discussion on this post has been closed. Please use my contact form to provide comments.