Introduction
This project is an upgrade of the Bulk Word Protection Utility posted here. Most of it's been re-written or re-factored since that article. It's a C# application used to search for and loop through all Word or Excel files and change protection settings in bulk.
Of course, the real usefulness is that you can bulk reset properties of Word documents fairly easily. For now, it just works with document protection, but you could mass modify any number of document properties with only a few more lines of code and some more options in the GUI.
Background
If you're not familiar with Word document protection, some info is available here: http://office.microsoft.com/en-us/word/CH010397751033.aspx.
Using the Code
There is a FileSearchClass
class, a ProcessDocuments
class, and some UI code behind the OfficeAppProtectionForm
form.
On the OfficeAppProtectionForm
form, there are five regions of code:
- Form Events: Contains the
InitializeComponent
and selects a document type on load. - Click Events: All [control]
_Click
events. - UI Events: Miscellaneous UI events. Many call other methods on the form.
- BackgroundWorker Events: Contains the
_DoWork
, _ProgressChanged
, and _RunWorkerCompleted
events for the two BackgroundWorker
processes. - Private Methods: Just a few grunts to do my UI bidding.
I'll spare you the mundane UI code and get into the BackgroundWorker
s and classes. First up, the xSearchFolderBackground
BackgroundWorker
. I will link a separate article just for this class. It's kind of cool by my standards since it estimates the number of sub-directories, accepts a BackgroundWorker
from the calling form, and sends updates back to it, and you can have a somewhat accurate ProgressBar
.
When you click the xSelectDirectory
button and choose a directory to search, the selected directory and the search pattern for the selected document type get appended together and passed to the xSearchFolderBackground.RunWorkerAsync
method. Then, the xSearchFolderBackground_DoWork
event, which is now running on the BackgroundWorker
's thread, creates a new FileSearchClass
, sends the BackgroundWorker
to the class, splits apart the directory and search pattern, and starts the search. I'll show the details of the FileSearchClass
in the other article. Here's the code for the click event and the BackgroundWorker
events:
private void xSelectDirectory_Click(object sender, EventArgs e)
{
this.DoubleBuffered = true;
if (this.xFolderBrowserDialog.ShowDialog() == DialogResult.OK)
{
if (xSearchFolderBackground.IsBusy != true)
{
if (this.xFilesListBox.Items.Count > 0)
if (MessageBox.Show("Would you like to clear the already " +
"added filenames before searching?",
"Files Already Listed",
MessageBoxButtons.YesNo) == DialogResult.Yes)
{
this.xFilesListBox.Items.Clear();
}
string searchPattern = string.Empty;
if (this.xDocTypeComboBox.Text == "Word Documents")
searchPattern = "*.doc";
else
searchPattern = "*.xls";
this.xDocProgressBar.Visible = true;
ChangeControlStatus(false);
this.Refresh();
xSearchFolderBackground.RunWorkerAsync(
this.xFolderBrowserDialog.SelectedPath +
"|" + searchPattern);
}
}
}
private void xSearchFolderBackground_DoWork(object sender, DoWorkEventArgs e)
{
string[] args = (e.Argument as string).Split("|".ToCharArray());
string directoryToSearch = args[0];
string searchPattern = args[1];
FileSearchClass searchClass = new FileSearchClass();
searchClass.bw = (BackgroundWorker)sender;
searchClass.Search(directoryToSearch, searchPattern);
}
private void xSearchFolderBackground_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
if (this.xDocProgressBar.Maximum >= e.ProgressPercentage)
{
this.xDocProgressBar.Value = e.ProgressPercentage;
}
else
{
}
if (e.UserState != null && e.UserState is FileSearchClass.UserState)
{
FileSearchClass.UserState state = (FileSearchClass.UserState)e.UserState;
if (state.currentOperation == FileSearchClass.Operations.Estimating)
this.xStatusLbl.Text =
string.Format("Status: {0}", state.estimatingMessage);
else if (state.currentOperation == FileSearchClass.Operations.Searching)
{
this.xStatusLbl.Text = string.Format("Status: {0}",
state.searchingMessage);
if (state.foundFileName != string.Empty)
{
this.xFilesListBox.Items.Add(state.foundFileName);
this.xFilesListBox.Refresh();
}
}
}
}
private void xSearchFolderBackground_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (!(e.Error == null))
{
MessageBox.Show(e.Error.Message, "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
ChangeControlStatus(true);
UpdateNumListboxItems();
this.xStatusLbl.Text = string.Format("Status: {0}", "Search complete!");
}
}
The ProcessDocuments
class does all of the heavy lifting when bulk processing the documents. The xGoButton
click event gathers all the UI information, sends it to a new ProcessDocuments
class, and passes the class to the xProcessDocsBackground.RunWorkerAsync
method:
private void xGoButton_Click(object sender, EventArgs e)
{
if (this.xGoButton.Text == "Cancel" &&
xProcessDocsBackground.WorkerSupportsCancellation &&
xProcessDocsBackground.IsBusy == true)
{
xProcessDocsBackground.CancelAsync();
ChangeControlStatus(true);
return;
}
if (this.xGoButton.Text == "Cancel" &&
xSearchFolderBackground.WorkerSupportsCancellation &&
xSearchFolderBackground.IsBusy == true)
{
xSearchFolderBackground.CancelAsync();
ChangeControlStatus(true);
return;
}
ArrayList fileNames = new ArrayList();
fileNames.AddRange(this.xFilesListBox.Items);
ArrayList userPasswords = new ArrayList();
if (this.xUnprotectPW.Text.Trim() == string.Empty)
{
userPasswords.AddRange(this.xUnprotectPW.Items);
}
else
{
userPasswords.Add(this.xUnprotectPW.Text.Trim());
userPasswords.AddRange(this.xUnprotectPW.Items);
}
WdProtectionType userWdProtectionType;
if (xNoProtectionRB.Checked)
userWdProtectionType = WdProtectionType.wdNoProtection;
else if (xTrackedChangesRB.Checked)
userWdProtectionType = WdProtectionType.wdAllowOnlyRevisions;
else if (xReadOnlyRB.Checked)
userWdProtectionType = WdProtectionType.wdAllowOnlyReading;
else if (xFillingFormsRB.Checked)
userWdProtectionType = WdProtectionType.wdAllowOnlyFormFields;
else
userWdProtectionType = WdProtectionType.wdAllowOnlyReading;
ProcessDocuments newDocInfoClass = new ProcessDocuments
{
fileNames = fileNames.ToArray(typeof(string)) as string[],
reprotectPassword = this.xProtectPW.Text,
passwords = userPasswords.ToArray(typeof(string)) as string[],
suggReadOnly = this.xReadOnlyCB.Checked,
wordProtectionType = userWdProtectionType,
bw = xProcessDocsBackground,
wordDocsToProcess = this.xDocTypeComboBox.Text.Contains("Word"),
xlDocsToProcess = this.xDocTypeComboBox.Text.Contains("Excel"),
removeXlProtection = this.xExcelNoProtectionRB.Checked
};
if (xProcessDocsBackground.IsBusy != true)
{
xProcessDocsBackground.RunWorkerAsync(newDocInfoClass);
ChangeControlStatus(false);
}
}
private void xProcessDocsBackground_DoWork(object sender, DoWorkEventArgs e)
{
ProcessDocuments passedDocInfo = e.Argument as ProcessDocuments;
passedDocInfo.ProcessOfficeDocs();
}
In the ProcessDocuments
class, I check to see if there are Word documents to process or Excel documents to process, and run either ProcessWordDocs()
or ProcessExcelDocs()
.
Those methods start an instance of either Word or Excel, opens each document in the list, tries every password in the list to unprotect the document (if necessary), and re-protects it according to the user's settings.
During processing, after every 10 documents processed, the code checks to see if more than 50% of the documents are failing (wrong password, incorrect permissions, etc.), and asks the user if they want to stop.
private void ProcessWordDocs()
{
if (_bw != null && _bw.WorkerReportsProgress)
{
_bw.ReportProgress(_bwPercentComplete, "Opening Word...");
}
Word.Application wordApp = new Word.Application();
Word.Document doc;
string errorFileNames = null;
string filesSavedSuccessfully = null;
double numErrorFiles = 0;
double numFilesSaved = 0;
double numFilesProcessed = 0;
int counter = 0;
for (int i = 0; i < _numFiles; i++)
{
object filename = fileNames[i];
if (_bw != null && _bw.WorkerReportsProgress)
{
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete, "Processing '" +
filename + "'");
}
try
{
doc = wordApp.Documents.Open(ref filename, ref missing,
ref objFalse, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref objFalse, ref missing,
ref missing, ref missing, ref missing);
if (doc.ProtectionType != Word.WdProtectionType.wdNoProtection)
{
try
{
object blankPassword = string.Empty;
doc.Unprotect(ref blankPassword);
}
catch { }
for (int j = 0; j < _passwords.GetLength(0); j++)
{
try
{
object unprotectPassword = _passwords[j];
doc.Unprotect(ref unprotectPassword);
}
catch { }
}
}
if (doc.ProtectionType == Word.WdProtectionType.wdNoProtection)
{
doc.Protect(wordProtectionType, ref objFalse,
ref _reprotectPassword, ref missing, ref missing);
object tempFilename = filename.ToString() + ".temp";
try
{
doc.SaveAs(ref tempFilename, ref missing, ref missing, ref missing,
ref objFalse, ref missing, ref _suggReadOnly,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing);
((Word._Document)doc).Close(ref objFalse, ref missing, ref missing);
System.IO.File.Delete(filename.ToString());
System.IO.File.Move(tempFilename.ToString(), filename.ToString());
numFilesSaved = numFilesSaved + 1;
numFilesProcessed = numFilesProcessed + 1;
filesSavedSuccessfully = filesSavedSuccessfully + filename +
System.Environment.NewLine;
}
catch (Exception)
{
errorFileNames = errorFileNames + filename +
System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
MessageBox.Show("Could not save the document: " + filename);
}
}
else
{
errorFileNames = errorFileNames + filename +
System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
((Word._Document)doc).Close(ref objFalse, ref missing, ref missing);
}
}
catch (Exception)
{
errorFileNames = errorFileNames + filename + System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
MessageBox.Show("Could not save the document: " + filename);
}
if (_bw != null && _bw.CancellationPending)
{
((Word._Application)wordApp).Quit(ref objFalse, ref missing, ref missing);
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
return;
}
if (_bw != null && _bw.WorkerReportsProgress)
{
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete);
}
counter = counter + 1;
if (counter == 10)
{
double percentFailed = (numErrorFiles / numFilesProcessed);
if (percentFailed >= 0.5)
{
if (MessageBox.Show(numErrorFiles +
" operations have failed out of " +
numFilesProcessed + ". Would you like to quit?",
"Operations Failed", MessageBoxButtons.YesNo,
MessageBoxIcon.Information) == DialogResult.Yes)
{
((Word._Application)wordApp).Quit(ref objFalse,
ref missing, ref missing);
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
return;
}
}
counter = 0;
}
}
((Word._Application)wordApp).Quit(ref objFalse, ref missing, ref missing);
WriteLogs(errorFileNames, filesSavedSuccessfully, numErrorFiles, numFilesSaved);
}
private void ProcessExcelDocs()
{
if (_bw != null && _bw.WorkerReportsProgress)
{
_bw.ReportProgress(_bwPercentComplete, "Opening Excel...");
}
Excel.Application xlApp = new Excel.Application();
Excel.Workbook xlWorkbook;
xlApp.DisplayAlerts = true;
xlApp.DisplayInfoWindow = true;
string errorFileNames = null;
string filesSavedSuccessfully = null;
double numErrorFiles = 0;
double numFilesSaved = 0;
double numFilesProcessed = 0;
int counter = 0;
for (int i = 0; i < _numFiles; i++)
{
string filename = fileNames[i];
if (_bw != null && _bw.WorkerReportsProgress)
{
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete, "Processing '" +
filename + "'");
}
try
{
xlWorkbook = xlApp.Workbooks.Open(filename, missing, objFalse, missing,
missing, missing, objTrue, missing, missing, missing,
missing, missing, missing, missing, missing);
foreach (Excel.Worksheet xlWorksheet in xlWorkbook.Sheets)
{
if (xlWorksheet.ProtectContents == true)
{
for (int j = 0; j < _passwords.GetLength(0); j++)
{
try
{
xlWorksheet.Unprotect(_passwords[j]);
}
catch { }
}
}
}
bool noErrors = true;
foreach (Excel.Worksheet xlWorksheet in xlWorkbook.Sheets)
{
if (xlWorksheet.ProtectContents == false)
{
if (!_removeXlProtection)
{
xlWorksheet.Protect(_reprotectPassword, objTrue, objTrue,
objTrue, missing, objFalse, objFalse, objFalse,
objFalse, objFalse, objFalse, objFalse,
objFalse, objFalse, objFalse, objFalse);
}
}
else
{
errorFileNames = errorFileNames + filename +
System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
xlWorkbook.Close(objFalse, missing, objFalse);
noErrors = false;
break;
}
}
if (noErrors == true)
{
object tempFilename = filename + ".temp";
try
{
xlWorkbook.SaveAs(tempFilename, missing, missing, missing,
_suggReadOnly, missing, Excel.XlSaveAsAccessMode.xlNoChange,
missing, missing, missing, missing, missing);
xlWorkbook.Close(objFalse, missing, objFalse);
System.IO.File.Delete(filename.ToString());
System.IO.File.Move(tempFilename.ToString(), filename.ToString());
numFilesSaved = numFilesSaved + 1;
numFilesProcessed = numFilesProcessed + 1;
filesSavedSuccessfully = filesSavedSuccessfully +
filename + System.Environment.NewLine;
}
catch (Exception)
{
errorFileNames = errorFileNames + filename +
System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
MessageBox.Show("Could not save the document: " + filename);
}
}
}
catch (Exception)
{
errorFileNames = errorFileNames + filename + System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
MessageBox.Show("Could not save the document: " + filename);
}
if (_bw != null && _bw.CancellationPending)
{
try
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
catch (Exception) { }
KillExcel();
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
return;
}
if (_bw != null && _bw.WorkerReportsProgress)
{
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete);
}
counter = counter + 1;
if (counter == 10)
{
double percentFailed = (numErrorFiles / numFilesProcessed);
if (percentFailed >= 0.5)
{
if (MessageBox.Show(numErrorFiles +
" operations have failed out of " +
numFilesProcessed + ". Would you like to quit?",
"Failing Operations", MessageBoxButtons.YesNo,
MessageBoxIcon.Information) == DialogResult.Yes)
{
try
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
catch (Exception) { }
KillExcel();
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
return;
}
}
counter = 0;
}
}
try
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
catch (Exception) { }
KillExcel();
WriteLogs(errorFileNames, filesSavedSuccessfully, numErrorFiles, numFilesSaved);
}
I ran in to a problem closing Excel which took me quite a while to figure out. Basically, you have to jump through a bunch of hoops to get Excel to release all of its COM resources, and it won't close until everything has been properly released.
So after hours of trying to make sure everything was closed properly, I finally just strong-armed Excel into closing by finding the instance of Excel.exe running in the Task Manager and closing the process with a blank MainWindowTitle
. Regular instances of Excel running have something in the MainWindowTitle
, so it will leave other open instances of Excel alone:
private void KillExcel()
{
foreach (Process xlProcess in Process.GetProcessesByName("Excel"))
{
if (xlProcess.MainWindowTitle == string.Empty)
xlProcess.Kill();
}
}
Then finally, the logs get written to the C:\ directory and are launched automatically:
private void WriteLogs(string errorFileNames, string filesSavedSuccessfully,
double numErrorFiles, double numFilesSaved)
{
const string errorsFileDir = "c:\\errors.log";
const string reportFileDir = "c:\\successes.log";
System.IO.FileStream fileStream;
try
{
fileStream = System.IO.File.Create(reportFileDir);
System.IO.StreamWriter objWriter;
objWriter = new System.IO.StreamWriter(fileStream);
objWriter.Write(filesSavedSuccessfully +
"______________________________________________" +
System.Environment.NewLine + numFilesSaved.ToString() +
" file(s) processed successfully.");
objWriter.Close();
MessageBox.Show("Success log created at: " + reportFileDir,
"Log Written", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception)
{
MessageBox.Show("Could not create report log at: " + reportFileDir,
"Log Written", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
try
{
fileStream = System.IO.File.Create(errorsFileDir);
System.IO.StreamWriter objWriter;
objWriter = new System.IO.StreamWriter(fileStream);
objWriter.Write(errorFileNames +
"______________________________________________" +
System.Environment.NewLine + numErrorFiles.ToString() + " problem file(s).");
objWriter.Close();
MessageBox.Show("Error log created at: " + errorsFileDir,
"Log Written", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
catch (Exception)
{
MessageBox.Show("Could not create error log at: " + errorsFileDir,
"Log Written", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
ProcessStartInfo procStartInfo = new ProcessStartInfo();
procStartInfo.FileName = reportFileDir;
Process.Start(procStartInfo);
procStartInfo.FileName = errorsFileDir;
Process.Start(procStartInfo);
}
Points of Interest
Like I said, you could modify this application to include other document properties to mass modify. The document subject, author, keywords, revision number, or any other document properties which are editable.
The search class is very reusable, and you can find out more about it here. It doesn't search inside ZIP files, and doesn't include shortcuts, so the number of files you find won't match exactly what your OS will find.
History
My inspiration for the method to access the Word properties came partly from here. Other than that, I've taken bits and pieces of information from the Web as I needed, mostly just concepts though and not actual code.
My previous article, Bulk Word Processing Utility, obviously could only do Word documents, and the search functionality froze the whole program. I've also done a bunch of re-factoring and performance tuning, and now it should be a bit faster and more resilient.
The program is now compatible with Office 2010.