Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

WPF Track Changes Viewer

4.40/5 (4 votes)
18 May 2011CPOL4 min read 30.4K   504  
Compare two text strings and show them in Word like track changes format

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 strings 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:

  1. Create a Word document with first text string
  2. Create another Word document with second text string
  3. Compare the 2 documents to get the track changes markup
  4. Convert the Word document with track changes markup visible to .XPS or .PDF format
  5. 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:

  1. Microsoft Office 2007
  2. 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.

  1. Add a reference to the Microsoft Word 12.0 Object Library to the Visual Studio project.

    Image 1

  2. Add reference to ReachFramework.dll that will host the functionality for XPS documents.

    Image 2

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:

XML
<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:

Image 3

Step 4: Import correct assemblies in MainWindow.xaml.cs as:

C#
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.

C#
Word._Application oWord; 

Step 5: On button click event, write the following code snippet:

C#
private void btnCompare_Click(object sender, RoutedEventArgs e)
{
    // get the executable path and set temp file names and path
    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
    {
        // start word application in the background
        oWord = new Word.Application();
        oWord.Visible = false;

        // delete previous occurrences of same files if any
        if (File.Exists(strDoc1)) File.Delete(strDoc1);
        if (File.Exists(strDoc2)) File.Delete(strDoc2);
        if (File.Exists(strXPSDoc)) File.Delete(strXPSDoc);

        // Create doc1.docx with text from textBox1
        this.CreateDocument(strDoc1, textBox1.Text);

        // Create doc2.docx with text from textBox2
        this.CreateDocument(strDoc2, textBox2.Text);

        // compare doc1.docx with doc2.docx and keep the track 
        // changes result in doc1.docx
        this.DocumentCompare(strDoc1, strDoc2);

        // convert doc1.docx to ".XPS" format by keeping the track 
        // changes markups visible
        this.ConvertDocument(strDoc1, strXPSDoc);

        // Delete temp word files
        File.Delete(strDoc1);
        File.Delete(strDoc2);

        // Create document viewer dialog box window
        System.Windows.Window myWindow = new System.Windows.Window();
        DocumentViewer docViewer = new DocumentViewer();
        myWindow.Content = docViewer;

        // read xps document
        XpsDocument xps = new XpsDocument(strXPSDoc, System.IO.FileAccess.Read);

        docViewer.Document = xps.GetFixedDocumentSequence();
        myWindow.ShowDialog();

        // delete the temporary xps file
        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
    {
        // Quit Word and release the ApplicationClass object.
        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).

C#
// start Word application in the background
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:

C#
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.

C#
private void CreateDocument(string pstrFileName, string pstrText)
{
    object oMissing = System.Reflection.Missing.Value;
    object oEndOfDoc = "\\endofdoc"; /* \endofdoc is a predefined bookmark */

    Word._Document oDoc;
    oDoc = null;
    try
    {
        oDoc = oWord.Documents.Add
	(ref oMissing, ref oMissing, ref oMissing, ref oMissing);

        //Insert a paragraph at the beginning of the document.
        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
    {
        // Close and release the Document object.
        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:

C#
private void DocumentCompare(string pstrDoc1, string pstrDoc2)
{
    object oMissing = System.Reflection.Missing.Value;
    //create a readonly variable of object type and assign it to false.
    object readonlyobj = false;
    object filename = pstrDoc1;
    //create a word document object and open the above file..
    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;//This will 
       			//keep the result of comparison in the first word document.
        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
    {
        // Close and release the Document object.
        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:

C#
public void ConvertDocument(string sourceDocPath, string targetFilePath)
{
    // Make sure the source document exists.
    if (!System.IO.File.Exists(sourceDocPath))
                throw new Exception("The specified source document does not exist.");
    Word._Document oDoc;
    oDoc = null;
    // Declare variables for the Documents.Open and 
    // ApplicationClass.Quit method parameters.
    object paramSourceDocPath = sourceDocPath;
    //object oMissing = Type.Missing;
    object oMissing = System.Reflection.Missing.Value;
    // Declare variables for the Document.ExportAsFixedFormat method parameters.
    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; //This is 
    //the key to keep track changes markup;
    bool paramIncludeDocProps = true;
    bool paramKeepIRM = true;
    WdExportCreateBookmarks paramCreateBookmarks =
                WdExportCreateBookmarks.wdExportCreateWordBookmarks;
    bool paramDocStructureTags = true;
    bool paramBitmapMissingFonts = true;
    bool paramUseISO19005_1 = false;
    try
    {
        // Open the source document.
        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);

        // Export it in the specified format.
        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
        {
            // Close and release the Document object.
            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.

C#
WdExportItem paramExportItem = WdExportItem.wdExportDocumentWithMarkup;

Finally, use the below code snippet to open a dialog box with DocumentViewer with xps document open:

C#
// Create document viewer dialog box window
System.Windows.Window myWindow = new System.Windows.Window();
DocumentViewer docViewer = new DocumentViewer();
myWindow.Content = docViewer;
// read xps document
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:

C#
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:

Image 4

When you hit Compare button, the btnCompare_Click will fire and you see the below result:

Image 5

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.

C#
// delete the temporary xps file
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)