Seamless inline text editing with ASP.NET AJAX
AJAX, ASP.NET, JavaScript, Performance, UI By Dave Ward on August 23rd, 2007
This is a technique that I really like. It’s excellently suited to intranet sites and administrative interfaces, where your users are typically familiar enough with the application to know which text they can click to edit. It’s actually very easy to implement, with a small amount of JavaScript.
To get started, we need a web form to display the inline text Label and a hidden TextBox that will be swapped in for editing:
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true"> <scripts> <asp:ScriptReference Path="behaviors.js" /> </scripts> </asp> <asp:Label runat="server" ID="Label1" /> <asp:TextBox runat="server" ID="TextBox1" Style="display: none;" />
Note that instead of using the Visibility property of the TextBox, I used CSS to hide it. This is important because ASP.NET renders no HTML for invisible server controls, making them unavailable for client side manipulation.
For the sake of brevity, I’m going to use the Cache to populate our Label. You could just as easily use a database, file, or other data store instead.
protected void Page_Load(object sender, EventArgs e) { if (Cache["Title"] != null) Label1.Text = Cache["Title"].ToString(); else Label1.Text = "Default Title (click to edit)"; }
Using JavaScript to toggle the elements
First, we’re going to need an Application Init handler to set up client side event handlers on the Label and TextBox.
>All of the following JavaScript code blocks are excerpts from behaviors.js.
var Label1, TextBox1; Sys.Application.add_init(AppInit); function AppInit(sender) { Label1 = $get('Label1'); TextBox1 = $get('TextBox1'); $addHandler(Label1, "click", Label1_Click); $addHandler(TextBox1, "blur", TextBox1_Blur); $addHandler(TextBox1, "keydown", TextBox1_KeyDown); }
In addition to wiring up the event handlers, I’ve set up a couple global variables to hold references to the Label and TextBox elements. This avoids excessive $get() calls and improves readability a bit.
Next, let’s handle the click event of our Label:
function Label1_Click() { TextBox1.value = Label1.innerHTML; Label1.style.display = 'none'; TextBox1.style.display = ''; // Thanks, Ira. TextBox1.focus(); }
A few things happen here:
- First, to make sure everything stays in sync, the TextBox’s value is always overwritten with the Label’s current text before showing it.
- Then, by toggling the display properties of the two elements, we effectively make an instant swap from Label to TextBox.
- Finally, using the focus() method of the TextBox will place the cursor in the newly displayed TextBox.
This sequence of events creates an intuitive user experience. It’s just as if clicking the text simply placed the cursor into a text editing field that had been there all along.
Next, we’ll handle the blur event of the TextBox.
function TextBox1_Blur() { Label1.innerHTML = TextBox1.value; TextBox1.style.display = 'none'; Label1.style.display = ''; // Thanks, Ira. }
This updates the Label with the TextBox’s new value and then toggles both elements’ visibility again. This is just the inverse of the Label1_Click() function.
The enter key is a habitual troublemaker
A problematic scenario is when the user presses enter while editing. Pressing enter in the TextBox will submit the form and generate an undesired postback. In fact, this scenario is probably more likely than the user blurring the TextBox to finish editing.
Luckily, the solution is simple:
function TextBox1_KeyDown(event) { if (event.keyCode == 13) { event.preventDefault(); // Thanks, Alessandro. TextBox1.blur(); } }
What this does is intercept any enter key press, cancel the pending form submission, and then blur the TextBox so that things proceed as desired.
Put it back, before anyone notices it’s missing
If we don’t save the edits back to the server somehow, none of this is worth much. The UpdatePanel and __doPostBack() combination comes to mind as one way to save, via partial postback. However, UpdatePanels have dangerous performance drawbacks, I’m going to avoid using them here.
Instead, I think this is yet another great situation for using JSON and page methods.
First, we’ll need a static method for saving to our data store. As I mentioned earlier, I’m using the Cache for simplicity, but in your code you could just as easily update a database field or write to a file here.
[WebMethod] public static void SetTitle(string title) { HttpContext.Current.Cache["Title"] = title; }
Note: Since an instance of the Page class is not created during a static page method callback, HttpContext isn’t directly available in our method. Due to this, the Cache class needs to be referenced as shown. Simply using Cache["Title"] won’t work in a static method.
So, now we’ve got a mechanism to save our data. Now, all we have to do is modify our TextBox’s blur event handler to utilize it:
function TextBox1_Blur() { Label1.innerHTML = TextBox1.value; TextBox1.style.display = 'none'; Label1.style.display = 'inline'; PageMethods.SetTitle(TextBox1.value); }
That’s it!
Notice that there isn’t an UpdatePanel anywhere on the page. Those things aren’t nearly as necessary as you may believe! Using JSON makes this work very quickly, efficiently, and unobtrusively.
Ways to make it better (always)
This is just the beginning. A few potential improvements include:
- Check the TextBox’s content against the Label on blur, and only call SetTitle() if the content has actually been edited.
- If working with a slow data store, progress indication may be beneficial.
- Resize the TextBox, based on the size of the Label.
- Use some sort of background image to indicate that the content is editable inline.
- Handle the ESC keyCode (27) and “cancel” the edit by switching back to the Label without changing its content.
- Can you think of any other improvements? Let’s hear about them in the comments.
Give it a try for yourself (this source includes the first suggested improvement):
Update: This functionality is also available as an ASP.NET server control. Inline Edit Box .NET
Similar posts
What do you think? Your comments are welcomed.
I appreciate all of your comments, questions, and other feedback, but please try to stay on topic. If you have a question unrelated to this post, I recommend posting on the ASP.NET forums. If you post there and then contact me with a link to the post, I'll try to take a look at it for you.
If you're replying to an existing comment, please use the threading feature. To do this, click the "Reply to this comment" link underneath the comment you're replying to.
Trackbacks
- 24 Links Today (2007-08-23)
- August 30th Links: ASP.NET, ASP.NET AJAX, IIS7, Visual Studio, Silverlight, .NET - ScottGu's Blog
- Inline Editing Makes Content Management A Snap - Joshua Stengel
- Wöchentliche Rundablage: ASP.NET MVC, ASP.NET 3.5, Data Services, C# 3.0, WPF, System.AddIn, AJAX… | Code-Inside Blog
- 10 Edit In-Place Ajax Scripts | WebTecker the latest tech, web resources and news.
- Web sayfalarınız için yerinde düzenleme (edit in place) | Göstergeç.net
- Edit In-Place Ajax Scripts « Renganathan’s Weblog


Comments
Awesome post.
I’ve seen this done on Google calendar when you edit an event. When you hover over the text of the What, Where, When, etc.. the cursor turns into the text cursor and the background changes to appear like a textbox. It’s very slick.
I haven’t seen that before. That’s an interesting way to do it.
Do they switch it back to regular text when you mouse out, or does the edit mode stick until you do something else (like press enter)?
The edit mode sticks on mouse out. Hitting enter has no effect.
I do not understand how you can reference your controls this way:
Label1 = $get(’Label1′);
TextBox1 = $get(’TextBox1′);
Label1 and TextBox1 are the server ID’s if I am correct. So how does this work?
Those are just global variable names. They could just as well be foo and bar.
I used those globals to avoid having to use $get() calls for every single reference to the elements.
I ment the $get(’Label1′); (Label1 inside the $get) that is a server side id.
Oh, sorry. I misunderstood.
Yes, the server side id is the same as the client side id for a simple page.
What I’m referencing with $get() is the rendered ClientID of the span and input elements, but they happen to render with the same DOM IDs as their corresponding controls’ server side IDs.
That’s typically the case, unless the controls are nested. In that case, you can make a call to the control’s ClientID property in order to get things straight. Like so:
http://encosia.com/2007/08/08/robust-aspnet-control-referencing-in-javascript/
You have a point Virrr!
‘Label1 = $get(’Label1′)’ will not work in a aspx page that uses masterpages. The client id will be something like ‘ctl00:PageContentPlaceHolder:Label1′. But it will work on a ‘normal’ aspx page.
Anyway, it’s a very nice feature to have on a website and turning it into a servercontrol will easily solve above problems.
Great article!
I have come to find it best to never set the “display” property to anything other than “none” or blank due to it toying with some of my other css rules.
For example:
function TextBox1_Blur() {
Label1.innerHTML = TextBox1.value;
TextBox1.style.display = ‘none’;
Label1.style.display = ”;
PageMethods.SetTitle(TextBox1.value);
}
This will still get your label to re-display in an inline fashion, but using the css default rather than block or inline.
Ira, you’re absolutely right. Good call.
Dave,
I cannot get this solution to work with Master and Content pages.
I have a ScriptManager with EnablePageMethods=true on Master page. Everything - the WebMethod and the client objects and script - is defined in the Master page, the content has nothing to do with it (think of it as a menu collapse/expand functionality with storing the collapse state in session).
The Javascript debugger says that “PageMethods” is not defined.
I believe page methods aren’t currently supported on master pages (only aspx pages).
It’s slightly more work, but you can use a regular web service call in almost exactly the same way you would a page method. That’s probably what you’ll need to do in this case.
The alternative is to use an UpdatePanel and use __doPostBack to trigger it in TextBox1_Blur().
Dave,
Thanks very much for your answer.
I have found useful the solution posted here: http://forums.asp.net/p/1084533/1634867.aspx “creating a common base class for all your pages and implementing the Web Method in that class would workaround the problem”
Only now I have to remember to inherit ALL the content pages of my Master page from the specific base class that contains the Web Method.
It’s a shame Master pages cannot contain Web Methods that would go into the content page’s PageMethods, don’t you think? :)
Thanks again,
Andy
When I open your demo in design mode I get a Render Control Error.
My source download doesn’t have a web.config, so the ScriptManager is giving you that error. I’ll update that download to include a web.config.
You could also use the default ASP.NET AJAX web.config.
I need more than 100 of this kind of control on my aspx page, it works great. but when i see the source code of that page in runtime, it gets huge, all i see is repeated java function for all editbox.
Is that ok to have? its almost got more then 10000 lines.
Are you using the Inline Edit Box .NET control, or doing it by hand like in this example?
This doesn’t work on VS2008 .NET3.5. Can you compile a new version of it? Thanks
This code works fine in VS2008. If you’re talking about the inline editing control, you need to post on that page’s comments, so Mike sees it.
Sorry my bad. I found my old comment in that thread. I wondered…
Thanks Dave!
I’ve created the same functionality and added database support. Pure javascript with a little C#.
http://tinyurl.com/38cj8h
Can somebody PLEASE give an example on how to carry out a server side code execution via the CallBackFunction…
Thnx
In the example, SetTitle() is a server side function that’s executed through the ScriptManager from the client side. What specifically are you having trouble doing?
Great example, it works great in IE, but I can’t seem to get it to work in FireFox.
I’m using the current version (2.0.0.14).
Anything I can do to get this working in both without too much trouble?
Thanks a bunch and keep up the great work!
Nevermind, I figured out what the problem was and it was all mine :)
Thanks again!