If you’ve used the Excel object model, youmay have discovered how incredibly handy the SaveCopyAs function is. Essentially, it allows you to save a currently loaded Excel spreadsheet into some arbitrary file, without altering the state of the loaded copy. In other words, if the user has altered the spreadsheet loaded into Excel, but hasn’t saved it, after a “SaveCopyAs”, that copy of the sheet is still considered dirty, and the saved file on disk does not have the user’s changes saved within it.
This is a very wonderful thing!
Basically, it means you can save off the spreadsheet as it currently exists in memory, complete with all the users edits up to this point, perform any operation on that saved copy that you want, and then either keep that copy or throw it away, and the copy that the user is currently working on is not affected in anyway whatsoever!
Good stuff.
But Word has never had it!
I’d rectified this long ago, in VB6, but recently had a need for this capability again, in VB.net this time.
Turns out, it’s incredibly simple to implement with VB.net, and much more intuitive to.
With .net 3.5, you can even create the function as an Extension Method, and directly add it to the Word Document object interface! Very cool!
The thing is, I knew I’d done this before, but couldn’t remember exactly how. After quite some time googling this, I came across a post where it was theorized it might be possible to implement SaveCopyAs by using the Compare function in Word.
Hmm, I’d never even tried the Compare, but it sounded interesting. After a few false starts, I ended up with this:
Imports System.Runtime.CompilerServices
<System.Runtime.CompilerServices.Extension()> _
Public Sub SaveCopyAs(ByVal Document As Word.Document, ByVal FileName As String)
Document.Compare(Name:=Document.FullName, CompareTarget:=Microsoft.Office.Interop.Word.WdCompareTarget.wdCompareTargetNew)
With Document.Application.ActiveDocument
If .Revisions.Count > 0 Then
.Revisions.RejectAll()
.SaveAs(FileName, AddToRecentFiles:=False)
.Close()
End If
End With
Document.Activate()
End Sub
Notice that I’m using the CompilerServices.Extension attribute to flag this as an extension method. This is what adds this method directly to the Word Document object and makes it so much more intuitive to use.
Essentially, the idea here is to:
- Compare the version of the document loaded in memory to the last saved version out on disk.
- Write the comparison result to a new document in memory (i.e. what becomes the active document)
- Check the revision count.
- If there are revisions, you know there’s been changes made to the document since it was last saved, so Reject all the revisions, save the active document and close it.
- If there were no revisions, the user hasn’t made any changes to the document since the last time they saved, so there’s really nothing else to do.
- Reactivate the user’s originally active document
The real trick here was step 4. Originally, I was accepting the revisions and then saving, and it wasn’t working at all. Eventually I traced the problem back to the comparison order. It turns out, when you perform a compare to another file from a loaded Document object in Word, the results of the comparison are the changes required to convert from the version of the document you currently have open in Word to the other document.
So, if the user has added the word “Nostradamus” in a paragraph, then the comparison would yield a revision that says, essentially, “remove the word Nostradamus from this paragraph”. This is exactly backward from the behavior you want. Turns out that all I had to do was “Reject all changes” instead.
Believe it or not, this works a dream.
But…
I can’t imagine that performing a compare, especially on a large document, is going to be a particularly speedy process and I knew I’d done this before using a alternate interface that the Word Document object implemented, albeit undocumentedly so. I just couldn’t remember how…
Suffice it to say, eventually I stumbled across the long forgotten interface. That interface is the COM IPersistFile. It’s a basic interface COM typically uses when saving Structured Storage documents, which are what Word files usually are (though, fortunately, it works with Word 2007 for saving DOCX style files).
Turns out the Word Document object implements that interface. They just don’t make that fact common knowledge.
Using it is trivially easy, especially given that it’s a COM interface that the .net framework happens to define natively (though do note, you’ll need to Import the InteropServices.ComTypes Namespace as show below.
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices.ComTypes
<System.Runtime.CompilerServices.Extension()> _
Public Sub SaveCopyAs(ByVal Document As Word.Document, ByVal FileName As String)
Dim pf = DirectCast(Document, IPersistFile)
pf.Save(FileName, False)
'pf.SaveCompleted(FileName)
End Sub
About that last commented line, that calls the SaveCompleted function. From the docs, it sounds like that is necessary, but I’ve never found a situation where it made a difference calling it or not.
Maybe someone can illuminate that aspect of the interface better than me?
But at any rate, the IPersistFile method is likely to be many times faster than the Compare method, especially for large files or files that the user has made many changes to without saving. Still, I thought the Compare method was interesting enough to warrant a mention.
So there you have it. SaveCopyAs for Word, implemented as an extension method that directly extends the Word Document object.
Yet more VB.net sweetness!