Introduction
I was working on a WPF Smart Client application where the user requested an ability to see what has been changed since last time in an editable text field (we maintain a history table for that field in order to see all previous edits). They wanted to be able to see the changes in Microsoft Word's track changes like format. I searched through the net and couldn't find a ready made solution. Then I decided to write my own control to get this functionality.
I knew it will be too difficult to write our own algorithm to get Word like track changes feature. Then I inquired with users and found that all of them had Microsoft Office 2007 installed. This was good news as I could exploit Microsoft Word's Compare function to achieve the objective.
Background
The basic idea was to generate a Word document which would contain a comparison of two text string
s in a track changes format and somehow show this document on a WPF window with track changes markup visible.
WPF has a DocumentViewer
control which can show .XPS or .PDF document. It means in order to show the document on WPF window, I had to convert it to .XPS or .PDF in such a way that it retains track changes markup.
All the following put together are the steps to create this control:
- Create a Word document with first text string
- Create another Word document with second text string
- Compare the 2 documents to get the track changes markup
- Convert the Word document with track changes markup visible to .XPS or .PDF format
- Show the resulting .XPS or .PDF using WPF
DocumentViewer
Using the Code
In order to use this code, you must have the following two pieces of software installed:
- Microsoft Office 2007
- 2007 Microsoft Office Add-in: Microsoft Save as XPS. It can be downloaded here.
Follow the steps below to make this work:
Step 1: Download the code.
Download the code and open the Project file.
Step 2: Add reference to required assemblies.
Once you have the above two prerequisites installed, you will need to reference the following assemblies in the code provided.
- Add a reference to the Microsoft Word 12.0 Object Library to the Visual Studio project.
- Add reference to ReachFramework.dll that will host the functionality for XPS documents.
Note: To add reference to these assemblies, you right click on Add Reference on the project name in Solution Explorer. On the .NET Framework, select ReachFramework
and other assemblies from the list and click OK button.
Step 3: Create WPF Form
Create a WPF form and name it "MainWindow.xaml". The WPF form will contain two text boxes and a button. The text in the two text boxes will be compared on click of "compare" button and the result will be shown in a dialog box having WPF DocumentViewer
.
The XAML for WPF form should look like this:
<Window x:Class="WPFTrackChangesTextBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Track Changes Demo" Height="340" Width="464">
<Grid Width="430">
<TextBox Height="100" HorizontalAlignment="Left"
Margin="15,12,0,0" Name="textBox1" VerticalAlignment="Top"
Width="400" TextWrapping="WrapWithOverflow" />
<TextBox Height="100" HorizontalAlignment="Left"
Margin="15,141,0,0" Name="textBox2" VerticalAlignment="Top"
Width="400" TextWrapping="WrapWithOverflow" />
<Button Content="Compare" Height="23" HorizontalAlignment="Left"
Margin="168,255,0,0" Name="btnCompare" VerticalAlignment="Top"
Width="75" Click="btnCompare_Click" />
</Grid>
</Window>
The output of the above XAML will look like this:
Step 4: Import correct assemblies in MainWindow.xaml.cs as:
using System.IO;
using Microsoft.Win32;
using System.Windows.Xps.Packaging;
using Microsoft.Office.Interop.Word;
using Word = Microsoft.Office.Interop.Word;
using System.Reflection;
Also, create a global object to open Word application.
Word._Application oWord;
Step 5: On button click event, write the following code snippet:
private void btnCompare_Click(object sender, RoutedEventArgs e)
{
string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
path = path.Substring(0, path.LastIndexOf(@"\"));
string strDoc1 = path + @"\doc1.docx";
string strDoc2 = path + @"\doc2.docx";
string strXPSDoc = path + @"\Doc.xps";
object oMissing = System.Reflection.Missing.Value;
try
{
oWord = new Word.Application();
oWord.Visible = false;
if (File.Exists(strDoc1)) File.Delete(strDoc1);
if (File.Exists(strDoc2)) File.Delete(strDoc2);
if (File.Exists(strXPSDoc)) File.Delete(strXPSDoc);
this.CreateDocument(strDoc1, textBox1.Text);
this.CreateDocument(strDoc2, textBox2.Text);
this.DocumentCompare(strDoc1, strDoc2);
this.ConvertDocument(strDoc1, strXPSDoc);
File.Delete(strDoc1);
File.Delete(strDoc2);
System.Windows.Window myWindow = new System.Windows.Window();
DocumentViewer docViewer = new DocumentViewer();
myWindow.Content = docViewer;
XpsDocument xps = new XpsDocument(strXPSDoc, System.IO.FileAccess.Read);
docViewer.Document = xps.GetFixedDocumentSequence();
myWindow.ShowDialog();
Uri xpsUri = xps.Uri;
System.IO.Packaging.Package oPackage =
System.IO.Packaging.PackageStore.GetPackage(xpsUri);
oPackage.Close();
System.IO.Packaging.PackageStore.RemovePackage(xps.Uri);
if (File.Exists(strXPSDoc)) File.Delete(strXPSDoc);
}
catch(Exception exp)
{
MessageBox.Show("There was an error!!! " + exp.Message);
}
finally
{
if (oWord != null)
{
oWord.Quit(ref oMissing, ref oMissing, ref oMissing);
oWord = null;
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Instantiate the Word application and set its visibility to false
(as shown above).
oWord = new Word.Application();
oWord.Visible = false;
The comments in the code are pretty much self explanatory. Now I will explain the following important functions called in the btnCompare_Click
event:
CreateDocument
DocumentCompare
ConvertDocument
Please note that in each of the above functions, I will create a Word document object "oDoc
" as:
Word._Document oDoc;
oDoc
will be used to create, open, compare or save a Word document. It is important to close and destroy this object in each function after it is used.
Below is the code snippet and explanation for all 3 functions.
CreateDocument
function accepts two parameters text and document name to be created. Below is how CreateDocument
function looks like. This function is called twice to create 2 Word documents each holding text from text box.
private void CreateDocument(string pstrFileName, string pstrText)
{
object oMissing = System.Reflection.Missing.Value;
object oEndOfDoc = "\\endofdoc";
Word._Document oDoc;
oDoc = null;
try
{
oDoc = oWord.Documents.Add
(ref oMissing, ref oMissing, ref oMissing, ref oMissing);
Word.Paragraph oPara1;
oPara1 = oDoc.Content.Paragraphs.Add(ref oMissing);
oPara1.Range.Text = pstrText;
object filename = pstrFileName;
oDoc.SaveAs(pstrFileName, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing);
}
catch (Exception e)
{
throw e;
}
finally
{
if (oDoc != null)
{
oDoc.Close(ref oMissing, ref oMissing, ref oMissing);
oDoc = null;
}
}
}
DocumentCompare
function accepts two file names to be compared and keeps the result of comparison in the first document. It looks like:
private void DocumentCompare(string pstrDoc1, string pstrDoc2)
{
object oMissing = System.Reflection.Missing.Value;
object readonlyobj = false;
object filename = pstrDoc1;
Word._Document oDoc;
oDoc = null;
try
{
oDoc = oWord.Documents.Open(ref filename, ref oMissing, ref readonlyobj,
ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing);
string filenm = pstrDoc2;
object compareTarget =
Microsoft.Office.Interop.Word.WdCompareTarget.wdCompareTargetCurrent;
object addToRecentFiles = false;
oDoc.Compare(filenm, ref oMissing, ref compareTarget, ref oMissing,
ref oMissing, ref addToRecentFiles, ref oMissing, ref oMissing);
}
catch (Exception e)
{
throw e;
}
finally
{
if (oDoc != null)
{
oDoc.Close(ref oMissing, ref oMissing, ref oMissing);
oDoc = null;
}
}
}
ConvertDocument
will convert document passed to it to .xps format by keeping the track changes markup visible. It looks like this:
public void ConvertDocument(string sourceDocPath, string targetFilePath)
{
if (!System.IO.File.Exists(sourceDocPath))
throw new Exception("The specified source document does not exist.");
Word._Document oDoc;
oDoc = null;
object paramSourceDocPath = sourceDocPath;
object oMissing = System.Reflection.Missing.Value;
string paramExportFilePath = targetFilePath;
WdExportFormat paramExportFormat = WdExportFormat.wdExportFormatXPS;
bool paramOpenAfterExport = false;
WdExportOptimizeFor paramExportOptimizeFor =
WdExportOptimizeFor.wdExportOptimizeForOnScreen;
WdExportRange paramExportRange = WdExportRange.wdExportAllDocument;
int paramStartPage = 0;
int paramEndPage = 0;
WdExportItem paramExportItem = WdExportItem.wdExportDocumentWithMarkup;
bool paramIncludeDocProps = true;
bool paramKeepIRM = true;
WdExportCreateBookmarks paramCreateBookmarks =
WdExportCreateBookmarks.wdExportCreateWordBookmarks;
bool paramDocStructureTags = true;
bool paramBitmapMissingFonts = true;
bool paramUseISO19005_1 = false;
try
{
oDoc = oWord.Documents.Open(ref paramSourceDocPath, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing);
if (oDoc != null)
oDoc.ExportAsFixedFormat(paramExportFilePath, paramExportFormat,
paramOpenAfterExport, paramExportOptimizeFor,
paramExportRange, paramStartPage, paramEndPage, paramExportItem,
paramIncludeDocProps, paramKeepIRM, paramCreateBookmarks,
paramDocStructureTags, paramBitmapMissingFonts,
paramUseISO19005_1, ref oMissing);
}
catch (Exception e)
{
throw e;
}
finally
{
if (oDoc != null)
{
oDoc.Close(ref oMissing, ref oMissing, ref oMissing);
oDoc = null;
}
}
}
The following line in the above code makes sure that resultant ".xps" has track changes markup.
WdExportItem paramExportItem = WdExportItem.wdExportDocumentWithMarkup;
Finally, use the below code snippet to open a dialog box with DocumentViewer
with xps
document open:
System.Windows.Window myWindow = new System.Windows.Window();
DocumentViewer docViewer = new DocumentViewer();
myWindow.Content = docViewer;
XpsDocument xps = new XpsDocument(strXPSDoc, System.IO.FileAccess.Read);
docViewer.Document = xps.GetFixedDocumentSequence();
myWindow.ShowDialog();
Also, make sure to close the Word application in finally
block as:
if (oWord != null)
{
oWord.Quit(ref oMissing, ref oMissing, ref oMissing);
oWord = null;
}
Step 6: Run the application. Here is how it will look like:
When you hit Compare button, the btnCompare_Click
will fire and you see the below result:
Points of Interest
One interesting thing I found while cleaning up was that I was not able to delete "Doc.xps" file and that was because it was locked by the process. I had to use the following code to delete it in order to make file name available for next time use.
Uri xpsUri = xps.Uri;
System.IO.Packaging.Package oPackage =
System.IO.Packaging.PackageStore.GetPackage(xpsUri);
oPackage.Close();
System.IO.Packaging.PackageStore.RemovePackage(xps.Uri);
if (File.Exists(strXPSDoc)) File.Delete(strXPSDoc);
History
- 9th May, 2011: Initial version