MSWord automation:Get file contents after it was saved - c#

I have an application that uses MSWord automation to edit some documents, after they save and close word I need to grab the modified file and put it back on the repository, there is only one scenario where I can't get it to work and that is
when the user makes changes to the file, selects to close word and selects yes to save the file
there are 2 events that I'm using:
DocumentBeforeSave
Quit
on the Quit event I'm trying to load the .docx file from disk but on this particular scenario I get an IOException because the file is still in use, somehow I need to wait until after the Quit event has been processed, which is when Word is actually closed and the file is no longer being used
right now I have it working using this
word.Visible = true;
while (!wordDone) { //gets changed to true on the Quit event
System.Threading.Thread.Sleep(100);
}
bool error = false;
do {
try { //need to load the contents of the modified file
ls.Content = System.IO.File.ReadAllBytes(provider.GetFileName());
error = false;
}
catch (System.IO.IOException) {
error = true;
System.Threading.Thread.Sleep(200);
}
} while (error);
while this works it is very ugly, I need a way to fire an event after the Quit event has been handled, or block the current thread while word is still running, or get an event after the document has been saved, the bottom line is I need a clean way to load the file after it has been saved and word is closed. DocumentAfterSave would be awesome, but doesn't seem to exist.
I Also tried unhooking the Quit handler and calling word.Quit on the Quit handler, that made no difference
I'm also investigating the use of ManualResetEvent or related classes, so far it almost works, but I still need to pause after it has been signaled to make sure word is closed and the file is no longer in use

I faced similar problem in the past as well. I dont think there is any nice clean way but instead of doing it like your above, how about considering this (will suit if you have a controlled environment)
Create word app
Get the Process ID immediately by using GetProcesses matching Winword and the last one in the list return should be the one you are after. This is not 100% reliable in multiuser environment.
After word quit, use the Thread.Sleep loop to ensure the PID no longer exist.
Reading the docx for your custom operations

I used to have the same problem. Using ReleaseComObject on all COM-related objects did the trick (that is, on your Word document object and your Word.Application object). That way you ensure that all dirty locks are removed after the COM object has been destroyed. Close the document and application with the Interop API. I use:
var localWordapp = new Word.Application();
localWordapp.Visible = false;
Word.Document doc = null;
// ...
if (doc != null)
{
doc.Close();
System.Runtime.InteropServices.Marshal.ReleaseComObject(doc);
}
localWordapp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(localWordapp);

Related

Delete .xlsx or .pdf after closing file

I'm trying to delete .xlsx or .pdf files after using them. When files are created I display them, but then users want automatic file deletion after closing them.
I've tried couple of things, but none of them seem to work properly. Issue:
When opened multiple files (.xlsx or .pdf) I can't terminate a single process, like just a single file. Instead what happens is that file get's deleted only when I close all same processes (Excel or PDF files). As I investigated this happens because Excel or PDF works as one instance only. However code works as expected when I have only one file opened...
This is what I have so far:
var process= Process.Start(file_path); //file_path is global variable
Set_event(process);
private void Set_event(Process process)
{
process.EnableRaisingEvents = true;
process.Exited += new EventHandler(Delete_File);
}
public void Delete_File(object sender, EventArgs e)
{
//Delete file on close
File.Delete(file_path);
}
I've also tried with DeleteOnClose method of FileOptions, but unfortunally that doesn't display file to user and doesn't quite delete file immediately after using them, only after my win app is closed. That isn't my desired output, but at least files are deleted, so If I could fix that I would be partially satisfied too. Here is my line for that:
var open_file = new FileStream(file_path,FileMode.Open, FileAccess.ReadWrite,FileShare.ReadWrite, 512, FileOptions.DeleteOnClose);
With all that said, are there any other options I missed ? Thanks for help in advance.
I've tried almost everything I could find (different variations of Exited_Event for Process, monitoring with FileSystemWatcher, creating files with DeleteOnClose - even API), but none of them worked as expected.
Everything ends or fails with issue I described in first place - some apps, like Microsoft Excel or Adobe Acrobat uses one instance to open a file (.pdf or .xls/.xlsx), so you can't just reference a single file as object while you have opened more files. That means you either end up with an error when trying to assign Exited_event to single file, or no error but file gets deleted only when you close all files with same type...
BUT fortunate enough I figured out one thing: WHEN you have opened more than one file in question (.pdf or .xlsx) something happens in background of OS: If you loop through processes of same type at that time, you'll get a list of particular instance that is in use.
In other words, while you have 2 Excel files opened, loop through processes is showing you only a file which is currently active for "EXCEL" process.
So, that leaded me to a completely new approach that might solve this issue. In order to have a complete solution for this you have to:
1. Create a method to check whether file is no longer in use.
2. Set a Timer with a delay of 2 seconds, to make sure process really ends. Maybe this should be incremented for different purposes...
3. Set a Timer_tick event, where you loop processes to see whether particular file is listed as active, and If user has already closed this file. As described by other users this method isn't quite accurate, but with setting delay for Timer I think there shouldn't be any problems anymore.
Here is a complete code for this (for .pdf and .xlsx - that is what I needed):
//as global variable
System.Windows.Forms.Timer delete_file = new System.Windows.Forms.Timer();
Process.Start(file_path); //file_path is global variable
delete_file.Tick += new EventHandler(timer_Tick);
delete_file.Interval = (2000);
delete_file.Enabled = true;
delete_file.Start();
private void timer_Tick(object sender, EventArgs e)
{
Boolean file_is_opened = false;
// Loop processes and list active files in use
foreach (var process in Process.GetProcesses())
{
if (process.MainWindowTitle.Contains(Path.GetFileName(file_path)))
{
file_is_opened = true;
}
}
//If our file is not listed under active processes we check
//whether user has already closed file - If so, we finally delete It
if (file_is_opened==false)
{
if (!File_In_Use(new FileInfo(file_path)))
{
File.Delete(file_path);
delete_file.Enabled = false;
delete_file.Stop();
return;
}
}
}
private bool File_In_Use(FileInfo file)
{
//Method to check whether file is in use
FileStream stream = null;
try
{
//If file doesn't exist
if (!file.Exists)
{
return false;
}
stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
catch (IOException)
{
//File is unavailable:
//because someone writes to It, or It's being processed
return true;
}
finally
{
if (stream!=null)
{
stream.Close();
}
}
//File not locked
return false;
}
This is how I did It. It might not be a perfect solution, but that works for me on Win 10 with no errors so far.
If someone has a suggestion to fix upper code, please let me know. Otherwise I hope this will help someone in future as I noticed there were already some questions about this in past, with no proper answer.

Word Interop: Event to catch close of Word

My application uses Word interop for "active reporting" functionality. When the Word document is launched from the application, we set a EventWaitHandle to pause the application (creating a 'modal' effect) until the document is closed:
wh.WaitOne();
We have set an event on the word Application Quit event, where we then set the wait EventWaitHandle for the application to continue
wordGenerator.WordApplication.ApplicationEvents2_Event_Quit += WordApplication_ApplQuit;
private void WordApplication_ApplQuit()
{
wh.Set(); // signal that word has closed
wordGenerator.Dispose();
wordGenerator = null;
}
After this is called, the application then reads the document from the location it was stored and saves it into our database. All works great. UNLESS... the user makes changes in the document and doesn't CTRL+S but rather clicks close and gets prompted with the "would you like to save changes" prompt.
What happens in this instance that the quit event is fired as soon as you click close in Word, but Word is still open whilst the dialog to save changes is there. The application then continues to run and gets IO exceptions "Document is in use by another process" when trying to read the document to save to the database. Even waiting and retrying doesn't work as it seems that Word and the Application are waiting on eachother.
Is there another event I can use? I can't bypass the alert and automatically save as perhaps the user doesn't want to save.
Problem solved... easy one this time. Moved the dispose code above the .Set().
private void WordApplication_ApplQuit()
{
wordGenerator.Dispose();
wordGenerator = null;
wh.Set(); // signal that word has closed
}

Force loop containing asynchronous task to maintain sequence

Something tells me this might be a stupid question and I have in fact approached my problem from the wrong direction, but here goes.
I have some code that loops through all the documents in a folder - The alphabetical order of these documents in each folder is important, this importance is also reflected in the order the documents are printed. Here is a simplified version:
var wordApp = new Microsoft.Office.Interop.Word.Application();
foreach (var file in Directory.EnumerateFiles(folder))
{
fileCounter++;
// Print file, referencing a previously instantiated word application object
wordApp.Documents.Open(...)
wordApp.PrintOut(...)
wordApp.ActiveDocument.Close(...)
}
It seems (and I could be wrong) that the PrintOut code is asynchronous, and the application sometimes gets into a situation where the documents get printed out of order. This is confirmed because if I step through, or place a long enough Sleep() call, the order of all the files is correct.
How should I prevent the next print task from starting before the previous one has finished?
I initially thought that I could use a lock(someObject){} until I remembered that they are only useful for preventing multiple threads accessing the same code block. This is all on the same thread.
There are some events I can wire into on the Microsoft.Office.Interop.Word.Application object: DocumentOpen, DocumentBeforeClose and DocumentBeforePrint
I have just thought that this might actually be a problem with the print queue not being able to accurately distinguish lots of documents that are added within the same second. This can't be the problem, can it?
As a side note, this loop is within the code called from the DoWork event of a BackgroundWorker object. I'm using this to prevent UI blocking and to feedback the progress of the process.
Your event-handling approach seems like a good one. Instead of using a loop, you could add a handler to the DocumentBeforeClose event, in which you would get the next file to print, send it to Word, and continue. Something like this:
List<...> m_files = Directory.EnumerateFiles(folder);
wordApp.DocumentBeforeClose += ProcessNextDocument;
...
void ProcessNextDocument(...)
{
File file = null;
lock(m_files)
{
if (m_files.Count > 0)
{
file = m_files[m_files.Count - 1];
m_files.RemoveAt(m_files.Count - 1);
}
else
{
// Done!
}
}
if (file != null)
{
PrintDocument(file);
}
}
void PrintDocument(File file)
{
wordApp.Document.Open(...);
wordApp.Document.PrintOut(...);
wordApp.ActiveDocument.Close(...);
}
The first parameter of Application.PrintOut specifies whether the printing should take place in the background or not. By setting it to false it will work synchronously.

Run a process for at least 5 seconds in C# WPF

My goal is to force a process to run for at least 5 seconds (any amount of time really). I am working of the .NET Framework 3.5, with Service Pack 1. The idea is that the document holds some information that the user must see, so to safe guard against them immediately clicking to close the document, we can force it to stay open for some time. I developed a small test UI, consisting of a button, and then three radio buttons (one for each document). Here is my code behind...
I define the strings for the file paths, the string for the chosen file's path, and int to store the process's ID, a boolean for if they can exit the program, and the thread and timer declarations such as..
string WordDocPath = #"Some file path\TestDoc_1.docx";
string PowerPointPath = #"Some file path\Test PowerPoint.pptx";
string TextFilePath = #"Some file path\TestText.txt";
string processPath;
int processID;
bool canExit = false;
System.Threading.Thread processThread;
System.Timers.Timer processTimer;
In the constructor, I initialize the thread and timer, setting the thread's start method to a method called TimerKeeper(), and then I start the thread.
processTimer = new System.Timers.Timer();
processThread = new System.Threading.Thread(new System.Threading.ThreadStart(timeKeeper));
processThread.Start();
I have the timer set to count to 5 seconds, upon which it will set the canExit boolean to true.
public void timeKeeper()
{
processTimer.Elapsed += new System.Timers.ElapsedEventHandler(processTimer_Elapsed);
processTimer.AutoReset = false;
processTimer.Interval = 5000; //5000 milliseconds = 5 seconds
}
void processTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
canExit = true;
}
The rest is my button's click event, which deicides which file path to use to start the process, starts the timer, and then starts the process itself..
private void button1_Click(object sender, RoutedEventArgs e)
{
if ((bool)PowerPointRadioButton.IsChecked)
{
processPath = PowerPointPath;
}
if ((bool)WordDocRadioButton.IsChecked)
{
processPath = WordDocPath;
}
if ((bool)TextDocRadioButton.IsChecked)
{
processPath = TextFilePath;
}
try
{
canExit = false;
processTimer.Start();
while (!canExit)
{
processID = System.Diagnostics.Process.Start(processPath).Id;
System.Diagnostics.Process.GetProcessById(processID).WaitForExit();
if (!canExit)
{
processTimer.Stop();
MessageBox.Show("Document must remain open for at least 5 seconds.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
processTimer.Start();
}
}
}
catch (Exception ex)
{
MessageBox.Show("Error dealing with the process.\n" + ex.Message.ToString());
}
This actually works, for the most part. The user still can close the document, but if it has not been 5 seconds, it will reopen. Except for the word document (.docx). Things go smoothly for the powerpoint and text files, but the word document has some strange behavior (please note that all 3 files are in the same file directory). When I choose the word documents radio button and click the button, the word document opens, BUT I am also prompted with the message box from the catch block, alerting me that a "Object reference not set to an instance on an object" exception was thrown. This only occurs for the word document. Like I said, the word document still opens (I can see it's contents, just like the powerpoint or textfile). The exception causes the lines that check to see if they can exit to be skipped, so the document can close immediately, which is a problem.
Can anyone see my issue here? Or if there is a better way to doing all of this (I am a wpf/c# newbie)? I just don't understand why this only occurs for the word document, and not the powerpoint and text files.
If this is run on the user's desktop you are subject to the proper app being installed (e.g. Word) and how it is configured. If these are read only files on a share then I could convert them to XPS so you could show them in a DocumentViewer. And rather than force them to wait 5 seconds to click make them say yes to a dialog box that they have read and understand the document. Or have this on a page with an "I agree" button as MilkyWayJoe suggested.
The problem could be that the associated application is not the word application itself, but some intermediate application that launches word on your behalf.
To find out, keep a reference to the process object, and check if it has already terminated, what it's executable path is.
Having said that, why do you need this annoying behavior? You cant stop people from looking the other way. Is it supossed to fullfill some legal requirement or something?

Adobe Reader process fails when starting second instance

In our C# WinForms application, we generate PDF files and launch Adobe Reader (or whatever the default system .pdf handler is) via the Process class. Since our PDF files can be large (approx 200K), we handle the Exited event to then clean up the temp file afterwards.
The system works as required when a file is opened and then closed again. However, when a second file is opened (before closing Adobe Reader) the second process immediately exits (since Reader is now using it's MDI powers) and in our Exited handler our File.Delete call should fail because it's locked by the now joined Adobe process. However, in Reader we instead get:
There was an error opening this document. This file cannot be found.
The unusual thing is that if I put a debugger breakpoint before the file deletion and allow it to attempt (and fail) the deletion, then the system behaves as expected!
I'm positive that the file exists and fairly positive that all handles/file streams to the file are closed before starting the process.
We are launching with the following code:
// Open the file for viewing/printing (if the default program supports it)
var pdfProcess = new Process();
pdfProcess.StartInfo.FileName = tempFileName;
if (pdfProcess.StartInfo.Verbs.Contains("open", StringComparer.InvariantCultureIgnoreCase))
{
var verb = pdfProcess.StartInfo.Verbs.First(v => v.Equals("open", StringComparison.InvariantCultureIgnoreCase));
pdfProcess.StartInfo.Verb = verb;
}
pdfProcess.StartInfo.Arguments = "/N"; // Specifies a new window will be used! (But not definitely...)
pdfProcess.SynchronizingObject = this;
pdfProcess.EnableRaisingEvents = true;
pdfProcess.Exited += new EventHandler(pdfProcess_Exited);
_pdfProcessDictionary.Add(pdfProcess, tempFileName);
pdfProcess.Start();
Note: We are using the _pdfProcessDictionary to store references to the Process objects so that they stay in scope so that Exited event can successfully be raised.
Our cleanup/exited event is:
void pdfProcess_Exited(object sender, EventArgs e)
{
Debug.Assert(!InvokeRequired);
var p = sender as Process;
try
{
if (_pdfProcessDictionary.ContainsKey(p))
{
var tempFileName = _pdfProcessDictionary[p];
if (File.Exists(tempFileName)) // How else can I check if I can delete it!!??
{
// NOTE: Will fail if the Adobe Reader application instance has been re-used!
File.Delete(tempFileName);
_pdfProcessDictionary.Remove(p);
}
CleanOtherFiles(); // This function will clean up files for any other previously exited processes in our dictionary
}
}
catch (IOException ex)
{
// Just swallow it up, we will deal with trying to delete it at another point
}
}
Possible solutions:
Detect that the file is still open in another process
Detect that the second process hasn't really been fully exited and that the file is opened in the first process instead
I just dealt with this a couple of days ago.
When there is no instance open already, the document opens in a new instance directly.
When there is an instance already open, I believe that instance spawns a new instance which you don't actually get a handle to. What happens is control returns to your function immediately, which then goes and deletes the file before the new instance has had a chance to read the file -- hence it appears to not be there.
I "solved" this by not deleting the files immediately, but keeping track of the paths in a list, and then nuking all of them when the program exits (wrap each delete in a try/catch with an empty catch block in case the file has disappeared in the meantime).
I would suggest following approach:
Create files in user's temp directory (Path.GetTempPath). You can create some sub-folder under it.
Attempt to delete files only when last instance of process gets exited (i.e. you need to count number of processes that you had launched, on exit, decrement the count and when it becomes zero, attempt to delete (all) files that are open so far)
Try to clean-up created sub-folder (under temp directory) while starting and ending the application. You can even attempt for periodic clean-up using timer.

Categories

Resources