Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Online Rich Text Editor with Download as Word Option

0.00/5 (No votes)
7 Sep 2010 3  
Online Rich Text Editor with Mail Merge and download file as Word option

Introduction

This article describes how you can create online rich text editor which provides the following functionality:

  • Online Rich Text Editing
  • Saving the rich text
  • Downloading it as Word document
  • Mail merge features

screenshot.png

Background

I have been using Google Doc for a while now. I was wondering if I could create something similar using the coding knowledge that I have. After searching online, I did not find anything (open source) that could provide Google like functionality and mail merge. I did not want to write the richtext editor from scratch. After searching the internet, I found that FCKEditor) should serve my needs. Also the OpenXML SDK from Microsoft can be used for mail merge and conversion to docx file.

You will need the FCKEditor as online editor. (You can download it here.)

You can download OpenXML SDK here.)

Using the Code

Project Setup

Find below the steps to setup the project in Visual Studio (I am using Visual Studio 2008).

vs.png

  1. Download and unzip the source in the folder of your choice
  2. To simply use it, configure this folder as a virtual folder in IIS (Here is a link from MSDN explaining how you can do it)
  3. Open Visual Studio
    1. Click on open project of type Web Site
    2. Open this folder as website
    3. Add references to the "DocumentFormat.OpenXml" and "WindowsBase". You can get those when you install the OpenXML SDK 2.0. Alternatively, I have included these two DLLs in the lib folder.
    4. Run the project

Code Explained

The project contains one folder called "scripts". This folder has the FCKEditor files. It also has jquery file and one of our script files called "myAjax.js" (I will explain them later). You will also find a folder called "docs". This is the folder that will hold our generated Word documents.

In the root folder, we have three files:

  1. Default.aspx - The default web page
  2. downloader.ashx - This file handles our download logic
  3. web.config - Normal config file

Default.aspx

Let's first open Default.aspx file. In the <head> section, include the following references to the JavaScript files. These files will allow us to use the FCKEditor and also provide reference to JQuery and our JavaScript file:

<script src="scripts/ckeditor/ckeditor.js" type="text/javascript"></script>
<script src="scripts/ckeditor/adapters/jquery.js" type="text/javascript"></script>
<script type="text/javascript" src="scripts/jquery-1.3.2.min.js"></script>
<script src="scripts/myAjax.js" type="text/javascript"></script>
<script language="javascript" type="text/javascript">	

Let's skip to the HTML now. We will come back to other scripts in the head later. On the form, we simply add a header for our page:

Online Richtext editor with Mail Merge		

Then we take care of those browsers that may not have JavaScript turned on.

<noscript>
  
    Online Richtext editor requires JavaScript to run. 
    In a browser with no JavaScript support, like yours, you should still see 
    the contents (HTML data) and you should be able to edit it normally, 
    without a rich editor interface.	    

</noscript>		

Next, we add some instructions and a textarea. The textarea will serve as our richtext editor. We use the inbuilt FCKEditor method "CKEditor.Replace" to convert the textarea into FCKEditor richText editor. We also add a div tag to display our messages

   <p>
	This is online rich text editor sample with mail merge.<br />
	Use the text area below to type in your document. <br />
	Use the dropdowns below that have mail merge fields. 
   </p>
<textarea cols="50" id="editor1" name="editor1" rows="10"></textarea>
<!-- instantiate a new instance of CKEDITOR -->
<script type="text/javascript">
	//<![CDATA[
	// Replace the <textarea id="editor1"> with an CKEditor instance.
	var editor = CKEDITOR.replace('editor1',
	{
	    toolbar: 'myToolBar', skin: 'office2003', width: '60%'
	});
	//]]>
</script>
<div id="eMessage" />		

The last section is where we add the dropdown for mailmerge fields and a button to let user generate the Word document. As you can see, we have an onclick handler for the button. The ajaxDownloadDoc() function is in our JavaScript file "myAjax.js". Here we use the JQuery's AJAX method to post the data to our handler and generate the Word document. More about handler in the next section.

 Insert Merge Fields from here
    <select id="MergeFields" >
        <option value="0" selected="selected">Select Merge Fields</option>
        <option value="1">{^Title^}</option>
        <option value="2">{^FirstName^}</option>
        <option value="3">{^LastName^}</option>
    </select>    
<input type="button" name="saveAsWord" id="saveAsWord"   
	önclick="javascript:ajaxDownloadDoc()" value="Download as word" />	 

As promised earlier, here is the JavaScript that actually inserts the mailmerge fields in the richtext editor. We use JQuery to acertain that our form is loaded. Then we tap on to the onSelectChange event of our "MergeFields" dropdown. Here we call our function and insert the selected values in the FCKEditor.

oEditor.insertHtml(valueToInsert);

Here is the code:

//select the merge fields dropdown and implement the onchange event 
//to insert the selected value in the text area
$(document).ready(function() {
	$("#MergeFields").val('0');
	$("#MergeFields").change(onSelectChange);
});

function onSelectChange() {
	var selected = $("#MergeFields option:selected");
	var oEditor = CKEDITOR.instances.editor1;

	if (selected.val() != 0) {
		var valueToInsert = selected.text();
		// Check the active editing mode.
		if (oEditor.mode == 'wysiwyg') {
			// Insert the desired HTML.
			oEditor.insertHtml(valueToInsert);
		}
		else {
			alert('You must be on WYSIWYG mode!');
		}
	}
	$("#MergeFields").val('0');
}		

Default.aspx.cs

Let's look at the code behind for our default page. There isn't much we do here. We simply provide a place holder to save the contents of the richtext editor to your place of choice. I have not implemented the save to database or other places. You can add the code as you see fit.

protected void Page_Load(object sender, EventArgs e)
{
	string richText = Request["editor1"];
	if (!string.IsNullOrEmpty(richText))
	{
		string a = richText;
		///TODO - save this HTML values somewhere
	}
}		

downloader.ashx

As you would have noticed, we also have a handler file here "downloader.ashx". For more information about how handers are used, click here. We use this file to handle the Ajax post back event. In the ProcessRequest method, we create a new file name by using GUID. This will ensure that our file names are unique. We then call the SaveAsWord method which does the heavy lifting.

public void ProcessRequest (HttpContext context) 
{
	try
	{
		string fileName = Guid.NewGuid().ToString() + ".docx";
		string path = context.Server.MapPath("~/docs/") + fileName;
		if (!(string.IsNullOrEmpty(context.Request["what"])) 
		&& (context.Request["what"].ToLower() == "saveasword")
			&& !(string.IsNullOrEmpty(context.Request.Form[0])))
		{
			SaveAsWord(context.Request.Form[0], path, fileName);
		}
	}
	catch (Exception ex)
	{
		context.Response.ContentType = "text/plain";
		context.Response.Write(ex.ToString());
		System.Diagnostics.Trace.WriteLine(ex.ToString());
	}	
}		

SaveAsWord is a wrapper function which first creates a Word document for us. Then, it calls a function to replace the mailmerge fields with actual values. Finally, it calls a function that generates the final Word document.

private void SaveAsWord(string input, string fullFilePath, string fileNameOnly)
{
	CreateDocument(fullFilePath);
	input = ReplaceMailMerge(input);
	generateWordDocument(input, fullFilePath, fileNameOnly);
}		

Creating Word document using the OpenXML SDK is pretty straight forward. I took this code from MSDN sample and tweaked it a bit to suit our needs.

private void CreateDocument(string path)
{
	// Create a Wordprocessing document. 
	using (WordprocessingDocument myDoc = 
	WordprocessingDocument.Create(path, WordprocessingDocumentType.Document))
	{
		// Add a new main document part. 
		MainDocumentPart mainPart = myDoc.AddMainDocumentPart();
		//Create DOM tree for simple document. 
		mainPart.Document = new Document();
		Body body = new Body();
		Paragraph p = new Paragraph();
		Run r = new Run();
		Text t = new Text("");
		//Append elements appropriately. 
		r.Append(t);
		p.Append(r);
		body.Append(p);
		mainPart.Document.Append(body);
		// Save changes to the main document part. 
		mainPart.Document.Save();
	}
}		

The ReplaceMailMerge function simply finds and replaces the mailmerge fields with whatever values we seem fit. Currently, I have hardcoded these values. However, you can wire it up so that these values come from some datasource instead.

private string ReplaceMailMerge(string input)
{
	input = input.Replace("{^FirstName^}", "Billy");
	input = input.Replace("{^LastName^}", "Bob");
	input = input.Replace("{^Title^}", "Dr.");
	return input;
}		

generateWordDocument is our work horse. It has two sections, the first section actually fills in the content in the document we created in step 1. It then adds some skeletal body to the document. We are relying on the AddAlternativeFormatImportPart method to simply add our richtext as XHTML to the document so that we do not need to do all the parsing. We will let the SDK handle the parsing and creating the document. The second section sends back the path of the new document to AJAX caller.

public void generateWordDocument
	(string htmlMarkup, string fullFilePath, string fileNameOnly)
{
	try
	{
		/*----------- Generate the Document -----------------------*/
		//put some title
		string pageTitle = Guid.NewGuid().ToString();
		//open the document
		using (WordprocessingDocument wordDoc = 
			WordprocessingDocument.Open(fullFilePath, true))
		{
			//get the document
			MainDocumentPart mainPart = wordDoc.MainDocumentPart;
			int altChunkIdCounter = 1;
			int blockLevelCounter = 1;

			string mainhtml = "<html><head><style type='text/css'>
			.catalogGeneralTable{border-collapse: 
			collapse;text-align: left;} .catalogGeneralTable 
			td, th{ padding: 5px; border: 1px solid #999999; }
			</style></head>
			<body style='font-family:Trebuchet MS;font-size:.9em;'>" 
			+ htmlMarkup 
			+ "</body></html>";
			string altChunkId = String.Format("AltChunkId{0}", 
			altChunkIdCounter++);

			//Import data as html content using Altchunk
			AlternativeFormatImportPart chunk = 
			mainPart.AddAlternativeFormatImportPart
			(AlternativeFormatImportPartType.Html, altChunkId);

			//add the chunk to the doc
			using (Stream chunkStream = chunk.GetStream
			(FileMode.Create, FileAccess.Write))
			{
				//Encoding.UTF8 is important to remove 
				//special characters
				using (StreamWriter stringWriter 
				= new StreamWriter(chunkStream, Encoding.UTF8)) 
				{
					stringWriter.Write(mainhtml);
				}
			}

			AltChunk altChunk = new AltChunk();
			altChunk.Id = altChunkId;
			//insert the text in the doc
			mainPart.Document.Body.InsertAt(altChunk, blockLevelCounter++);
			//save the document
			mainPart.Document.Save();
		}
		/*----------- End Generate the Document -----------------------*/
		
		/* ------- Send the response -----------*/
		//clear the response object
		HttpContext.Current.Response.ClearContent();
		//add the demilited string to the response object and write it. 
		string url = HttpContext.Current.Request.ApplicationPath 
				+ "/docs/" + fileNameOnly;
		HttpContext.Current.Response.Write(url);
		HttpContext.Current.Response.End();

		/* -------End Send the response -----------*/
	}
	catch (Exception ex)
	{
		HttpContext.Current.Response.Write(ex.Message.ToString());
	}
}		

Other Files of Interest

I have modified the config.js file (found under root-->scripts-->ckeditor) to create my own custom toolbar for the FCKeditor. You do not need this if you are going to use the out of the box editor.

CKEDITOR.editorConfig = function(config) {
	config.toolbar = 'myToolBar';
	config.toolbar_myToolBar =
	[
		['Source', '-', 'Save', 'NewPage', 'Preview', '-', 'Templates'],
		['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 
			'Print', 'SpellChecker', 'Scayt'],
		['Undo', 'Redo', '-', 'Find', 'Replace', '-', 
			'SelectAll', 'RemoveFormat'],
		'/',
		['Bold', 'Italic', 'Underline', 'Strike', '-', 
			'Subscript', 'Superscript'],
		['NumberedList', 'BulletedList', '-', 'Outdent', 
			'Indent', 'Blockquote', 'CreateDiv'],
		['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
		['Link', 'Unlink', 'Anchor'],
		['Image', 'Table', 'HorizontalRule', 'SpecialChar', 'PageBreak'],
		'/',
		['Styles', 'Format', 'Font', 'FontSize'],
		['TextColor', 'BGColor'],
		['Maximize', 'ShowBlocks']
	];
};		

Points of Interest

As you would have noticed, I have kept it to the bare minimum. Here are some of the things we can improve upon:

  1. Currently, I do not know of a way for JQuery to send in binary data back on a AJAX call. Hence, we are sending the document path. You will notice that I also have a method called DownloadFile. If you do not want to use AJAX, you can modify the generateWordDocument method to call this method instead. It will then write the document as binary data to the response object.
  2. I have added the mail merge dropdown outside our editor. It will be nice if we could include it in the toolbar.
  3. The "Save" button on the editor toolbar does a postback. It would be nice we could do an AJAX call instead.
  4. I had to turn of ValidateRequest="false" in the default.aspx page to avoid ASP.NET throwing exception as we are modifying control content on fly. There must be a better way of handling this.

History

  • 7th September, 2010: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here