[C#] Word-like editor and RTF text manipulation with Telerik RadRichTextEditor
Introduction
Telerik is a famous company - acquired by Progress Software in 2014, which offers top-tier tools for software development, as advanced customized controls, libraries, and so on. Mainly focused on .NET development tools, more recently Telerik sells platform for web, hybrid and native app development. In this article, we will see how to create a Word-like software, using one of Telerik's controls for WinForms, i.e. RadRichTextEditor and C#. Moreover, we will discuss several ideas in regards to the possibility of a programmatical modification of the RTF text hosted by the control, in order to produce DOCX/PDF files compiled by our applications.
Pre-Requisites
- Microsoft Visual Studio >= 2015
- Telerik DevCraft Suite (any version), purchasable at https://www.telerik.com/purchase.aspx
For the present article, it will be assumed Telerik DevCraft Suite is already installed on the developing machine.
If not, Telerik provided a very straightforward how-to on running the setup. Please check the following link for WinForms libraries and controls installation: https://docs.telerik.com/devtools/winforms/installation-deployment-and-distribution/installing-on-your-computer
Create a new project
In Visual Studio, click "New Project", and select "Windows Form Application" template from Visual C#. Give your solution a name, then click OK.
Once your project has been created, take a look at the toolbox: if Visual Studio Extensions (from Telerik DevCraft) has been previously installed, Telerik's controls will show up. In the following image, the reader can see the highlighted RadRichTextEditor, and as any other control, it can be dragged on our form.
Running now the sample will show a simple Windows Forms with a RadRichTextEditor on it, but the latter is already fully usable. In the following image, it can be seen a sample of typed text. To modify parts of the text, adding bold or underline to what has been typed, it is possible to use known Microsoft Word shortcuts: pressing Ctrl+B will result in bold, Ctrl+U produces underlined text, and so on. For a full list of RadRichTextEditor's shortcuts, please refer to: https://docs.telerik.com/devtools/winforms/richtexteditor/keyboard-support
Add RichTextEditorRibbonBar for quick access to common features
Telerik provides a very neat ribbon bar, which can be used in conjunction with RadRichTextEditor, to have common commands at hand, making easier to modify text. The RichTextEditorRibbonBar can be easily dragged on form from the toolbox, and linked to the RichTextEditor by setting its AssociatedRichTextEditor property.
In running the sample, the reader can now test the features that the two controls union will produce: a pretty much full-fledged text editor, without having written any code so far.
Forewords for a case study
But what are some of the possibilities offered for a programmatically automated use of those controls?
We will see a very small case study in the following. Suppose a user have a set of already existant .docx models, among which he must choose and load the proper one, each of which contains some tags that must be replaced with a given text (for example, let's think about a precompiled letter, that must be completed by adding the recipient's name, and some other information. To further complicate things, suppose the letter must include the compiler's stamp).
We will break down the code that will be necessary to accomplish what listed above. Starting from the simplest tasks, we will see how to achieve each step programmatically. At the end of that analysis, it will be presented a complete class, named EMWordProcess.cs, and a small sample that uses it.
Save document in .Docx or .Pdf formats
Saving documents from RadRichTextEditor is pretty simple. The DocxFormatProvider and PdfFormatProvider provide a very immediate Export() method. In both cases, given a RadDocument object (the RichTextEditor contents, that can be read from the RadRichTextEditor Document property) and a path in which to save, if is sufficient to pass the document to the Export() method, along with an opened stream toward the destination path.
The following are two extension methods for RadDocument which will save a RadRichTextEditor.Document to .Docx and .Pdf files:
public static void SaveAsDocx(this RadDocument document, string path)
{
var provider = new DocxFormatProvider();
using (Stream output = new FileStream(path, FileMode.OpenOrCreate))
{
provider.Export(document, output);
}
}
public static void SaveAsPdf(this RadDocument document, string path)
{
var provider = new PdfFormatProvider();
using (Stream output = new FileStream(path, FileMode.OpenOrCreate))
{
provider.Export(document, output);
}
}
Load a previously edited .Docx files
The DocxFormatProvider can be used also to load/read .Docx files. In the following method, given the existing .Docx path, the file will be read and imported (by the method Import) to a RadDocument variable, which can be later assigned, for example, to the Document property of a RadRichTextEditor.
public static RadDocument LoadDocument(string path)
{
RadDocument document = null;
var provider = new DocxFormatProvider();
using (var stream = new FileStream(path, FileMode.Open))
{
document = provider.Import(stream);
}
return document;
}
Clone a document
Cloning a document can be useful in some cases. The following method has been implemented mainly because a RadDocument is always passed by reference. If we need to take a RadDocument and alter it, doing so will also alter the source RadDocument, and maybe we don't want to do this. That will be more clear when we'll see the merge method. For now, let's say that cloning will result in a exact copy of a RadDocument in another RadDocument. That function can be achieved by using XamlFormatProvider to access the data of the source document, and using the Export method to copy it on the second RadDocument.
public static RadDocument CloneDocument(this RadDocument document)
{
var copy = new RadDocument();
var provider = new XamlFormatProvider();
string data = provider.Export(document);
copy = provider.Import(data);
return copy;
}
Merge documents
Merge two or more document implies we have a list of RadDocuments, desiring to obtain a single document form their concatenation. Here a typical use of cloning can be observed: since the original file must not be altered, the first document will be cloned on a new RadDocument, then the caret will be moved at its end, and the n-th document will be inserted with the method InsertFragment, that allows inserting an RTF text to be added at the caret position.
public static RadDocument MergeDocuments(RadDocument[] documents)
{
if (documents[0] == null) return null;
RadDocument mergedDocument = CloneDocument(documents[0]);
for (int i = 1; i < documents.Length; i++)
{
if (documents[i] == null) continue;
mergedDocument.CaretPosition.MoveToLastPositionInDocument();
mergedDocument.InsertFragment(new DocumentFragment(documents[i]));
}
return mergedDocument;
}
Replace text
For replacing text, we will implement a method with the source document, text to be searched, text to be replaced as arguments.
First, we'll declare a DocumentTextSearch object, which will be used to perform a text search, identifying the positions of matching text (TextRange objects) and use those informations to apply the replacing text. For this mean we use two for-each loops: the first one, based on occurrences of search.FindAll(toSearch) method, will populate a list of TextRanges, while the second loop use that list to move the caret on each found position, inserting the replacing text in place of the old one.
public static void ReplaceText(RadDocument document, string toSearch, string toReplaceWith)
{
var search = new DocumentTextSearch(document);
var rangesTrackingDocumentChanges = new List<TextRange>();
foreach (var textRange in search.FindAll(toSearch))
{
var newRange = new TextRange(new DocumentPosition(textRange.StartPosition, true), new DocumentPosition(textRange.EndPosition, true));
rangesTrackingDocumentChanges.Add(newRange);
}
foreach (var textRange in rangesTrackingDocumentChanges)
{
document.CaretPosition.MoveToPosition(textRange.StartPosition);
document.DeleteRange(textRange.StartPosition, textRange.EndPosition);
var r = new RadDocumentEditor(document);
r.Insert(toReplaceWith);
textRange.StartPosition.Dispose();
textRange.EndPosition.Dispose();
}
}
Replace text with image
Replacing text with an image is accomplished - for the first part of the procedure - in the same way as replacing text: with a first for-each loop, the text to be replaced is searched for, populating a list of positions, or TextRanges. The second loop will go through that list, move the caret accordingly, and replace the found text with a FloatingImageBlock object, a particular RadDocument element that can obtain a picture from a stream.
In the case below, we declare a FloatingImageBlock of 160x120 px, which will host a JPG image. Please note the used MemoryStream must point to a valid JPG file. The we proceed in setting some properties, like WrappingStyle, to put image behind text, and AllowOverlap. With InsertInline method, the image will be inserted at required position.
public static void ReplaceWithImage(RadDocument document, string toSearch)
{
var search = new DocumentTextSearch(document);
var rangesTrackingDocumentChanges = new List<TextRange>();
foreach (var textRange in search.FindAll(toSearch))
{
var newRange = new TextRange(new DocumentPosition(textRange.StartPosition, true), new DocumentPosition(textRange.EndPosition, true));
rangesTrackingDocumentChanges.Add(newRange);
}
foreach (var textRange in rangesTrackingDocumentChanges)
{
document.CaretPosition.MoveToPosition(textRange.StartPosition);
document.DeleteRange(textRange.StartPosition, textRange.EndPosition);
var r = new RadDocumentEditor(document);
using (var imgStream = new MemoryStream(File.ReadAllBytes(@"<PATH_TO_A_VALID_JPG_FILE>")))
{
var imgInline = new FloatingImageBlock(imgStream, new Telerik.WinControls.RichTextEditor.UI.Size(160, 120), "jpg");
imgInline.VerticalPosition = new FloatingBlockVerticalPosition(Telerik.WinForms.Documents.Model.FloatingBlocks.VerticalRelativeFrom.Paragraph, -140);
imgInline.AllowOverlap = true;
imgInline.WrappingStyle = WrappingStyle.BehindText;
r.InsertInline(imgInline);
}
textRange.StartPosition.Dispose();
textRange.EndPosition.Dispose();
}
}
Full EMWordProcess class source code
The following is the complete source code for EMWordProcess.cs class file, which will be used to develop our case study:
using System.Collections.Generic;
using System.IO;
using Telerik.WinForms.Documents;
using Telerik.WinForms.Documents.FormatProviders.OpenXml.Docx;
using Telerik.WinForms.Documents.FormatProviders.Pdf;
using Telerik.WinForms.Documents.FormatProviders.Xaml;
using Telerik.WinForms.Documents.Model;
using Telerik.WinForms.Documents.TextSearch;
namespace TelerikRadTextEdSample
{
public static class EMWordProcess
{
public static void SaveAsDocx(this RadDocument document, string path)
{
var provider = new DocxFormatProvider();
using (Stream output = new FileStream(path, FileMode.OpenOrCreate))
{
provider.Export(document, output);
}
}
public static void SaveAsPdf(this RadDocument document, string path)
{
var provider = new PdfFormatProvider();
using (Stream output = new FileStream(path, FileMode.OpenOrCreate))
{
provider.Export(document, output);
}
}
public static RadDocument LoadDocument(string path)
{
RadDocument document = null;
var provider = new DocxFormatProvider();
using (var stream = new FileStream(path, FileMode.Open))
{
document = provider.Import(stream);
}
return document;
}
public static RadDocument CloneDocument(this RadDocument document)
{
var copy = new RadDocument();
var provider = new XamlFormatProvider();
string data = provider.Export(document);
copy = provider.Import(data);
return copy;
}
public static RadDocument MergeDocuments(RadDocument[] documents)
{
if (documents[0] == null) return null;
RadDocument mergedDocument = CloneDocument(documents[0]);
for (int i = 1; i < documents.Length; i++)
{
if (documents[i] == null) continue;
mergedDocument.CaretPosition.MoveToLastPositionInDocument();
mergedDocument.InsertFragment(new DocumentFragment(documents[i]));
}
return mergedDocument;
}
public static void ReplaceText(RadDocument document, string toSearch, string toReplaceWith)
{
var search = new DocumentTextSearch(document);
var rangesTrackingDocumentChanges = new List<TextRange>();
foreach (var textRange in search.FindAll(toSearch))
{
var newRange = new TextRange(new DocumentPosition(textRange.StartPosition, true), new DocumentPosition(textRange.EndPosition, true));
rangesTrackingDocumentChanges.Add(newRange);
}
foreach (var textRange in rangesTrackingDocumentChanges)
{
document.CaretPosition.MoveToPosition(textRange.StartPosition);
document.DeleteRange(textRange.StartPosition, textRange.EndPosition);
var r = new RadDocumentEditor(document);
r.Insert(toReplaceWith);
textRange.StartPosition.Dispose();
textRange.EndPosition.Dispose();
}
}
public static void ReplaceWithImage(RadDocument document, string toSearch, string imagePath)
{
var search = new DocumentTextSearch(document);
var rangesTrackingDocumentChanges = new List<TextRange>();
foreach (var textRange in search.FindAll(toSearch))
{
var newRange = new TextRange(new DocumentPosition(textRange.StartPosition, true), new DocumentPosition(textRange.EndPosition, true));
rangesTrackingDocumentChanges.Add(newRange);
}
foreach (var textRange in rangesTrackingDocumentChanges)
{
document.CaretPosition.MoveToPosition(textRange.StartPosition);
document.DeleteRange(textRange.StartPosition, textRange.EndPosition);
var r = new RadDocumentEditor(document);
using (var imgStream = new MemoryStream(File.ReadAllBytes(imagePath)))
{
var imgInline = new FloatingImageBlock(imgStream, new Telerik.WinControls.RichTextEditor.UI.Size(160, 120), "jpg");
imgInline.VerticalPosition = new FloatingBlockVerticalPosition(Telerik.WinForms.Documents.Model.FloatingBlocks.VerticalRelativeFrom.Paragraph, -140);
imgInline.AllowOverlap = true;
imgInline.WrappingStyle = WrappingStyle.BehindText;
r.InsertInline(imgInline);
}
textRange.StartPosition.Dispose();
textRange.EndPosition.Dispose();
}
}
}
}
Developing a simple app
To develop the case study mentioned above, we need to drag a RadRichTextEditor and a RichTextEditorRibbonBar (binded to the editor) on a WinForm, and create the EMWordProcess class as listed.
Now, suppose we have a simple .Docx file like the following:
In the app, the bracketed word will be used as tags, so that they can be easily identified. CURRENTDATE tag will be used to expose the current date, RECIPIENT will be replaced with the name of the recipient of the letter, while SENDER will be the name of the letter's author. STAMP is the tag that we will replace with an image, hypothetically representing the sender's stamp.
In the Load() event of the form, that .Docx file can be simply loaded by using our LoadDocument() method, from EMWordProcess class:
Replacing tags can be done with the methods seen above.
Consider the following snippet:
EMWordProcess.ReplaceText(radRichTextEditor1.Document, "{CURRENTDATE}", DateTime.Now.ToLongDateString());
EMWordProcess.ReplaceText(radRichTextEditor1.Document, "{RECIPIENT}", "Mr. Smith");
EMWordProcess.ReplaceText(radRichTextEditor1.Document, "{SENDER}", "The author");
EMWordProcess.ReplaceWithImage(radRichTextEditor1.Document, "{STAMP}", @"c:\tmp\keyb.jpg");
Given the RadDocument read from radRichTextEditor1.Document property, we simply proceed in asking that a certain text/tag will be replaced by another text. In case of STAMP tag, we'll use the ReplaceWithImage() method, passing to it a valid JPG file path. Running our sample that way, will result in the following:
What was presented here is the simplest way to use the implemented text manipulation functions, but the code can be extended according to real needs. For example, we can produce a certain number of PDF files replacing tags with records coming from a SQL Server data source, or every other automation can be useful to achieve our production needs.
Demo
GIF file, if you cannot see animation, please open it in another tab
Source code
The source code used in the above samples can be freely downloaded at https://code.msdn.microsoft.com/C-Word-like-editor-and-RTF-a81219ef
Please note the downloadable package doesn't contain any of the Telerik libraries and/or controls. To acquire those components it is necessary to purchase a license (refer to the link in "Pre-Requisites" section)