(In which the question of getting TinyMCE to DataBind successfully in UpdatePanels is partially solved, but Chrome refuses to play nicely.)

There seems to be a common cry of agony when you try to stick a TinyMCE-adorned textarea in an UpdatePanel. Partial page updates strip the iFrame that TinyMCE’s init script lays onto the textarea. The UpdatePanel also seems to prevent TinyMCE’s built-in trigger from updating the underlying textarea in non-IE browsers (oh the irony) which means that if you try and update values to a database, the FormCollection at the back of the page never gets the new value and the database doesn't get updated.

TinyMCE's download page has an alpha .NET wrapper for TinyMCE which I’m using here by the way. Chris de Vos has used some of the solution described below to implement an AJAX extender control which applies TinyMCE to a designated control. My own version of ‘TinyMCE.NET’ which doesn’t use gzip compression but which does embed the TinyMCE scripts into the DLL can be downloaded here as a .NET 3.5 solution. If you have any improvements on it, please let me know. Download it here.

Textarea loses TinyMCE wrapper in Partial Postbacks

There are two approaches that .NET has for fixing this issue. The first is to use the page’s ScriptManager to re-run the call to TinyMCE.Init whenever the UpdatePanel containing the textarea is posted back asynchronously. So then, if we have a function which generates a call to TinyMCE.init (called GenerateInitCall), then we can just make this call.

ScriptManager.RegisterStartupScript(
   this, this.GetType(), this.ClientID + "_ajax", GenerateInitCall(), true);

The alternate approach comes from Stefan (steho706), who figured out how to use the MS AJAX javascript library to remove and then re-add the TinyMCE skin to the textarea. The original thread is here on forums.asp.net. His solution has three parts. It works best if your page contains a button that causes the partial postback.

1. When the partial postback is triggered, save the tinyMCE value back to the textarea associated with it. If you’re using a button of some sort to initiate the postback, that means associating the following function to its onClick (onClientClick if you’re doing it in .NET) event.

function UpdateTextArea()
{ 
    tinyMCE.triggerSave(false, true); 
} 

If you’re using something other than a button - for example, a command link in a DataGrid, you’ll need to attach the function to the initializeRequest event that the MS AJAX javascript library makes available for partial postbacks via its client-side PageRequestManager.

Sys.WebForms.PageRequestManager.
   getInstance().add_initializeRequest(UpdateTextArea);

If you’re not familiar with this, it works pretty much the same as the asp.net page lifecycle but is completely client-side. MSDN has a good introduction to it here.

2. As the partial postback request begins, unload the TinyMCE editor from the page. Note that the function tests whether the button clicked to cause the postback is the one associated with the TinyMCE textarea. As noted earlier this makes it more awkward if buttons aren't what is causing the postback. Use the PageRequestManager's BeginRequest event to trigger the function.

Sys.WebForms.PageRequestManager.
   getInstance().add_beginRequest(BeginRequestHandler); 

function BeginRequestHandler(sender, args)
{
   // Fix to make tinyMCE work with UpdatePanel 
   var elem = args.get_postBackElement();
   if (elem.id == '<%= btnSave.ClientID %>') // Only the button unloads the editor
   { 
      // Check that there is an instance to remove
      if (tinyMCE.getInstanceById('<%= txtContent.ClientID %>') != null && 
         tinyMCE.getInstanceById('<%= txtContent.ClientID %>') != "undefined")
      {
         tinyMCE.execCommand('mceFocus', false, '<%= txtContent.ClientID %>');    
         tinyMCE.execCommand('mceRemoveControl',false,'<%= txtContent.ClientID %>');
      }
   }
}

3. At this point, the value in the TextArea is saved back into the form and posted back to the server. When the page is posted back, TinyMCE must be loaded back onto the textarea. To achieve this, we attach one more function, this time to the PageRequestManager’s endRequest event.

Sys.WebForms.PageRequestManager.
   getInstance().add_endRequest(EndRequestHandler); 

function EndRequestHandler(sender, args)
{ 
   // Ensure that the editor is not already loaded 
   if (tinyMCE.getInstanceById('<%= txtContent.ClientID %>') == null || 
      tinyMCE.getInstanceById('<%= txtContent.ClientID %>') == "undefined")
   {
      tinyMCE.execCommand('mceAddControl',false,'<%= txtContent.ClientID %>');
   }
}

Both solutions work well. The former is simpler, but relies on the .NET ScriptManager to inject the code. The latter doesn’t rely on server-side code, but needs an identifiable trigger for the postback so that each event fires only against the desired TinyMCE area and not all the ones on the page. If you are using .NET, the latter needs to be registered as a StartupScript.

if (IsInUpdatePanel)
{
   Page.ClientScript.RegisterStartupScript(this.GetType(), 
      this.ClientID + "_ajax", GenerateMceAjaxFixScript(), true);
}

Two-way Binding To TinyMCE in an UpdatePanel

One further problem I discovered after making the changes noted above to TinyMCE’s alpha .NET wrapper was that while I could mark its text contents as [Bindable] in code, that two-way binding did not always work. Specifically when the TinyMCE control was in an UpdatePanel and changes were being made by a browser that wasn’t Internet Explorer (ironically). It seems that even if a call to TinyMCE.triggerSave is made as the request for a partial postback is initialized to update the textarea with the new value given by the user, that is still too late to have that new value saved to the FormValueCollection that is actually posted back to the server for binding to the server (as part of the page’s ViewState). The save is made in time by Internet Explorer but is not by Firefox or Chrome.

Fortunately, there is an easy partial solution to this problem for Firefox users. TinyMCE’s editor exposes a number of events including onChange which fires when a user has changed the text on the page. Adding a setup call in TinyMCE.init that handles onChange and also copies the new value to the textarea seems to do the trick.

tinyMCE.init({
   relative_urls:false,
   elements:'<% = elm.ClientID %>',
   ...,
   setup : function(ed) { 
      ed.onChange.add(function(ed, l) { 
         var hidden = document.getElementById('<% = elm.ClientID %>');
         hidden.value = l.content; }); }
});

So now we have two-way data binding working in IE and Firefox. Unfortunately it doesn’t work in Chrome, but hey - it’s a start. Download this code here.