I have a weird problem where Excel is behaving differently on my development machine and a testing machine.
In my add-in, I've turned off ScreenUpdating in several places for long running processes. On my machine this works fine. On the testing machine, Excel sets ScreenUpdating = true as soon as I write to a cell.
The following code demonstrates the issue for me.
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
Microsoft.Office.Interop.Excel.Application excel = Globals.ThisAddIn.Application;
MessageBox.Show(excel.ScreenUpdating.ToString());
excel.ScreenUpdating = false;
MessageBox.Show(excel.ScreenUpdating.ToString());
Workbook workbook = Globals.ThisAddIn.Application.ActiveWorkbook;
Worksheet w = (Worksheet)workbook.Worksheets[1];
((Range)w.Cells[1, 1]).Value = "Test";
MessageBox.Show(excel.ScreenUpdating.ToString());
}
On my machine, opening Excel gives three message boxes saying
"True", "False", "False".
On the test machine they say
"True", "False" and "True".
I've also stepped through with a remote debugger and watched the ScreenUpdating property change immediately after the cell value is set. Further, this isn't the only thing that resets ScreenUpdating. Adding or removing a Worksheet or Workbook will also do this.
The Excel version on each system is the same (14.0.6112.5000 (32-bit)).
What could be causing this? How can I fix it so that Excel respects my settings?
I am experincing the same problem with ScreenUpdating (and other settings) being reset to true by the Bloomberg add-in with VSTO add-in. Their support are working on a solution but in the meanwhile the following solution works ok:
By hiding each sheet when interacting with its cells the screen won't update and you'll get the performance benefits. Make sure to not try to hide the last sheet as it will raise an error. Not sure if this is suitable for your project but it is a workable solution for mine.
Here is some example code in VB.NET to hide a sheet:
' Create a book with a single worksheet
Dim Book As Excel.Workbook
Book = Globals.ThisAddIn.Application.Workbooks.Add(Excel.XlWBATemplate.xlWBATWorksheet)
'% Create sheet variable
Dim Sheet As Excel.Worksheet
Sheet = TryCast(Book.ActiveSheet, Excel.Worksheet)
' Visible
Sheet.Visible = Excel.XlSheetVisibility.xlSheetVisible
' Hidden but user able to show
Sheet.Visible = Excel.XlSheetVisibility.xlSheetHidden
' Hidden for user as well
Sheet.Visible = Excel.XlSheetVisibility.xlSheetVeryHidden
Other addins in Excel can interfere with that single global setting.
It is for that reason you are supposed to save the current ScreenUpdating state to a local variable before, and restore it after, each use.
Ignore the changing of that setting in the ThisAddIn_Startup event (as you would not normally do your work there anyway).
If an add-in is interfering with your ScreenUpdating somehow, you can stop them form doing it by disabling events temporarily. using EnableEvents. It is possible that this will break the functionality of that addon, but it worked fine for my needs.
Related
DataGridView populated with Worksheets & its visibility Status:
I have a Custom Task Pane for my Excel Addins with a DataGridView I populate from my open WorkBook:
Its content are all the Sheets of the open Workbook.
Rows are visible or not based on the Worksheets visibility Status (visible/hidden/SuperHidden).
Intercepting Worksheets Visibility change from the Addin?
I have operations/tasks happening in the Excel Workbook that the User does that can show/hide worksheets based on what s/he's doing. So,
Do I need to intercept a certain workbook event for that? (I would appreciate an example if this is the suggested solution please)
Do I need to call a Subroutine/function of the Addin from Excel after the running task? (I would appreciate how I can do that if this is what you suggest as well please)
But I prefer not to use thread because it's not everytime that the sheets are hidden/showing but only on certain occasions (Users actions). And it's only then that I need the DataGridView Visible worksheets List is required to update.
Your suggestions/recommendations regarding this is greatly appreciated. Please feel free to let me know should you ever need further information, I'll be glad to update this post or reply you in the comments.
It is better for you to call the Add-in void directly from your Excel:
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;
namespace YourSolution
{
[ComVisible(true)]
public interface IAddInUtilities
{
//You have to implement this
void YourFunctionToCall();
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class AddInUtilities : IAddInUtilities
{
//Implemntation: Call this from your Excel
public void YourFunctionToCall()
{
//The Operation here ...
}
}
}
Then from your Excel VBA, use this Sub:
Public Sub FunctionCallableInVBA()
Dim addIn As COMAddIn
Dim automationObject As Object
Set addIn = Application.COMAddIns("YourSolution")
Set automationObject = addIn.Object
'Call the Function your shared on the COM
automationObject.YourFunctionToCall()
Set addIn = Nothing
Set automationObject = Nothing
End Sub
This should solve your issue and avoid you to search desperately for that in-existing hide/unhide event.
I'm using the following code to run a VBA macro via C# Excel interop:
public void macroTest()
{
Excel.Application xlApp = new Excel.Application();
xlApp.Visible = true;
string bkPath = #"C:\somePath\someBk.xlsm";
Excel.Workbook bk = xlApp.Workbooks.Open(bkPath);
string bkName = bk.Name;
string macroName = "testThisMacro_m";
string runString = "'" + bkName + "'!"+macroName;
xlApp.Run(runString);
bk.Close(false);
xlApp.Quit();
}
testThisMacro_m is in a module testMacro, and this runs successfully. When I replace it with:
string macroName = "testThisMacro_s";
where testThisMacro_s has its code in Sheet1, the xlApp.Run() line gives the following COM Exception:
Cannot run the macro ''someBk.xlsm'!testThisMacro_s'.
The macro may not be available in this workbook or all macros may be disabled.
I checked macro security settings, and they are indeed set to "Disable with notification", but being able to run a macro from a module and not from a worksheet seems to indicate that this is a different issue than application-level macro security.
Is there something different that I have to do when making an interop call to a macro in a worksheet?
UPDATE: I was able to get the macro to execute by changing the call to:
string macroName = "Sheet1.testThisMacro_s"
but it seems that this hands control back to C# before the macro completes, so now I need to figure out how to check for macro completion (probably a different question).
A Worksheet object is an object - and objects are defined with class modules. Worksheets, workbooks, user forms; they're all objects. And you can't just call a method on an object, if you don't have an instance of that object.
Macros work off standard modules, which aren't objects, and don't need to be instantiated.
Application.Run can't call methods of an object, that's why macros need to be in standard modules.
I was able to get the macro to execute by changing the call to:
string macroName = "Sheet1.testThisMacro_s"
Wouldn't a helper sub solve both of your problems, re: Mat's Mug's reply concerning instantiation?
In some standard module:
Sub testHelperSubToBeCalledFromInterop
Call Sheet1.testThisMacro_s
End Sub
EDIT:
I am writing an Excel Add-in. Following is my code
private void ThisAddInStartup(object sender, EventArgs e)
{
Excel.Sheets sheets = Application.ActiveWorkbook.Sheets;
_worksheet = (from Excel.Worksheet sheet in sheets where sheet.Name.Contains(SheetName) select sheet).FirstOrDefault();
Application.SheetChange += ApplicationSheetChange;
}
When I debug, everything works great. But When I open an excel file directly from my hard drive then I am getting Application.ActiveWorkbook as null. Can anybody help me to understand this.
I want to start my add-in as when an excel file opens. Basically my add-in is tracking change in excel sheet of workbook and doing some required action.
If it matters, I am using Office 2007, Visual Studio 2012. I am able to run the solution after changing the project file and replacing Office 14 part with 12.
I assume you mean ThisAddIn_Startup and instead of ThisAddInStartup. If not then that is probably a problem.
It is recommended that you don't try to access a document inside the ThisAddin_Startup method. This is because Office doesn't always have a document ready when this method is run so you could run into some strange behavior. Instead, hook into an event that fires when the user opens a document and run your code there. It should look something like this (Note: I haven't tested this code but it should work):
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
// Hook into the workbook open event
this.Application.WorkbookOpen += new AppEvents_WorkbookOpenEventHandler(WorkWithWorkbook);
}
private void WorkWithWorkbook(Microsoft.Office.Interop.Excel.Workbook workbook)
{
// Workbook has been opened. Do stuff here.
}
Check out the MSDN article on writing Application-Level addins. Specifically pay attention to the part that talks about accessing a document when the application starts.
http://msdn.microsoft.com/en-us/library/vstudio/bb157876.aspx
An Excel workbook is called by a command line, which itself is launched from c# (3.5). The workbook runs, but there is an error in the VBA. For example, a column is missing in a pivot and Excel prompts the user with an error message, with the option to "debug".
From the process in C#, we can detect that the error window is open, and we take a screenshot of the error message, and then we close the error box.
If this was an interactive session, Excel would then present the VBA editor, in debug mode, with the line where the error occurred highlighted.
However, because this is running in an unattended terminal session, we are then unable to take a screenshot of the VBA editor (the screenshot is a black screen).
We are then able to close the excel program, using the windows PID.
The questions is: how do we either get a screenshot of the VBA editor, or how do we bind (with COM or interop) with the Excel in debug mode, and traverse the dom to find the error line, and possibly error message?
(1) If you have authoring control of the Excel workbook(s), you can insert line numbers and a error handler to write to file Err.Number, Err.Description, Err.Source, Erl.
(2) If (1) is not an option, but you can set Macro Security on the host to allow programmatic control of the Visual Basic project, then you can get the active line number:
Dim xl As excel.Application
Dim StartLine As Long, StartColumn As Long, EndLine As Long, EndColumn As Long
Set xl = GetObject(, "Excel.Application")
xl.VBE.ActiveCodePane.GetSelection StartLine, StartColumn, EndLine, EndColumn
Debug.Print StartLine, StartColumn, EndLine, EndColumn
(3) If neither (1) nor (2) is an option, it is a bit hairy, but you can copy out the bitmap contents of the window: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183402%28v=vs.85%29.aspx
The simplest solution if you have access to modify the VBA code might be to use On Error GoTo and a line label. This will stop the Debug message from popping up and instead, when a runtime error occurs, execution will jump to the specified label at which point you can access the information available in the global Err object. VBA does not have the concept of a try/catch block so this is pretty much the nearest proximally.
Sub Main()
On Error GoTo Catch
' Code that may trigger errors here...
GoTo Finally
Catch:
' Log Err info, etc...
Debug.Print "Error " & Err.Number & " - " & Err.Description
Finally:
' This is always reached last (error or no error)
End Sub
Snapshot of data structures, VBA excel
It sounds a lot like the question above in which I suggested to write the state of your code to a separate txt file (in the example, using a FileSystemObject) where you can afterwards analyze where you got stuck:
booleans on conditions / instantiation of objects, number on succesful loops, values contained in variables, well - whatever you want...).
In combination with an error handler (as suggested before) you can see where the code stopped + code / description of the error.
A copy-paste for your sake:
dim sFile As string
Dim FSO As FileSystemObject
Dim FSOFile As TextStream
sFile = "U:/Log.txt"
Set FSO = New FileSystemObject
Set FSOFile = FSO.OpenTextFile(sFile, 2, True)
FSOFile.writeline (<Descriptions+Property/variable value>)
FSOFile.Close set FSO = nothing
I agree that it is a more work, but you'll know where to find the bug.
All depends on how hard and often you need this, in combination with how long the code is that you need to describe. Since I am not aware of your situation, I cannot judge on that.
In the end, it seems a lot of work, but actually it's quite easy since it's just describing the code that is already there.
Hope it helps.
We use Excel interop in numerous places in our code, however I have one function which doesn't ever seem to close down the Excel process it uses.. I've simplified the code down, and it just seems like whenever I open a workbook in this function it stays hanging around. I've included the code below, I've made sure every object is defined, released and nulled, and yet Excel stays running.
System.Data.DataTable dtExcelSheet = new System.Data.DataTable();
Microsoft.Office.Interop.Excel.Application excelObject = new Microsoft.Office.Interop.Excel.Application();
dtExcelSheet.Columns.Add("SheetName", typeof(string));
dtExcelSheet.Columns["SheetName"].ReadOnly = false;
dtExcelSheet.Columns["SheetName"].Caption = "Sheet Name";
Workbooks wbs = excelObject.Workbooks;
Workbook excelWorkbook = wbs.Add(excelFile);
excelWorkbook.Close(false, System.Reflection.Missing.Value, System.Reflection.Missing.Value);
wbs.Close();
excelObject.Quit();
int i1 = Marshal.FinalReleaseComObject(excelWorkbook);
int i2 = Marshal.FinalReleaseComObject(wbs);
int i3 = Marshal.FinalReleaseComObject(excelObject);
excelWorkbook = null;
wbs = null;
excelObject = null;
GC.GetTotalMemory(false);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.GetTotalMemory(true);
Make sure you're not calling Excel on a background thread. I had a similar problem where I was cleaning up all the COM objects, but Excel still wasn't dying and that turned out to be the problem.
I wrote up my experiences and a solution here.
Complete guess, but does this row:
dtExcelSheet.Columns.Add("SheetName", typeof(string));
return the column that is created?
If so you'd probably need to store that reference and clean that up at the end.
Edit: Also, I don't think you should be setting the variables to null at the end, I think that just accesses them again.
And you shouldn't have to tell the GC to collect etc, but I assume that might be your test code.
It's been a while since I've mucked around with this stuff, so nothing jumps out at me.
My usual recommendation in these cases is to set your excel application object to Visible = true to see if there's not a dialog popping up on you. Excel/Word will sometimes refuse to shut down if they think there's a modal dialog open no matter what else you may do. It's the first thing to check anyway.
I tried running your code, but I could not reproduce the issue. If I step through it with a debugger, the Excel process terminates after the last call to FinalReleaseComObject. Is it possible that the culprit lies in some code not present in your listing?
When using COM interop, I have found that it is all too easy to increment the reference count on COM objects in very subtle ways. For example, let's say you do something like this:
excelWorkbook.Foo.Bar();
This can increment the reference count on the Foo object, leaving you with no means of releasing it afterwards...and leaving the Excel process lingering around until you shut down your app. You can re-write the above line of code like this:
Foo foo = excelWorkbook.Foo;
foo.Bar();
Marshal.ReleaseComObject(foo);
It's not as pretty, but it will decrement the reference count on the Foo object after you are done using it.
Same thing happened to me the other day. See my question on Excel Automation as to the fix (question deals with multiple row deletion mostly but I also had the same problem as you turns out it has to do with releasing all the COM objects properly).