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
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).
- Download and unzip the source in the folder of your choice
- 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)
- Open Visual Studio
-
- Click on open project of type Web Site
- Open this folder as website
- 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.
- 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:
- Default.aspx - The default web page
- downloader.ashx - This file handles our download logic
- 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>
<!---->
<script type="text/javascript">
//<![CDATA[
// Replace the <textarea id="editor1"> with an CKEditor instance.
var editor = CKEDITOR.replace( {
toolbar: });
//]]>
</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:
$(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();
if (oEditor.mode == 'wysiwyg') {
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;
}
}
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)
{
using (WordprocessingDocument myDoc =
WordprocessingDocument.Create(path, WordprocessingDocumentType.Document))
{
MainDocumentPart mainPart = myDoc.AddMainDocumentPart();
mainPart.Document = new Document();
Body body = new Body();
Paragraph p = new Paragraph();
Run r = new Run();
Text t = new Text("");
r.Append(t);
p.Append(r);
body.Append(p);
mainPart.Document.Append(body);
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
{
string pageTitle = Guid.NewGuid().ToString();
using (WordprocessingDocument wordDoc =
WordprocessingDocument.Open(fullFilePath, true))
{
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++);
AlternativeFormatImportPart chunk =
mainPart.AddAlternativeFormatImportPart
(AlternativeFormatImportPartType.Html, altChunkId);
using (Stream chunkStream = chunk.GetStream
(FileMode.Create, FileAccess.Write))
{
using (StreamWriter stringWriter
= new StreamWriter(chunkStream, Encoding.UTF8))
{
stringWriter.Write(mainhtml);
}
}
AltChunk altChunk = new AltChunk();
altChunk.Id = altChunkId;
mainPart.Document.Body.InsertAt(altChunk, blockLevelCounter++);
mainPart.Document.Save();
}
HttpContext.Current.Response.ClearContent();
string url = HttpContext.Current.Request.ApplicationPath
+ "/docs/" + fileNameOnly;
HttpContext.Current.Response.Write(url);
HttpContext.Current.Response.End();
}
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:
- 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.
- I have added the mail merge dropdown outside our editor. It will be nice if we could include it in the toolbar.
- The "Save" button on the editor toolbar does a postback. It would be nice we could do an AJAX call instead.
- 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