Excel Automation: Protect single worksheet from deletion by user - c#

VSTO 4.0 / Office 2007
In an Excel document-level automation project, I have a worksheet that must not be deleted from the workbook. I am afraid that a careless user might delete it by accident, which currently causes a lot of grief (exceptions galore).
I can not protect the entire workbook, because the user must be able to create, delete and otherwise modify this file. Exactly one sheet needs to be protected from deletion, but I may be overlooking something, so if there exists such solution I'm all ears. For example I could imagine that I can Protect() and Unprotect() the workbook when the sheet is visible, but this solution seems messy.
Googling yielded the following VBA code:
Private Sub Worksheet_Activate()
Dim CB As CommandBar
Dim Ctrl As CommandBarControl
For Each CB In Application.CommandBars
Set Ctrl = CB.FindControl(ID:=847, recursive:=True)
If Not Ctrl Is Nothing Then
Ctrl.OnAction = "RefuseToDelete"
Ctrl.State = msoButtonUp
End If
Next
End Sub
I'm not familiar with VBA, but I tried running this from the VSTO generated startup method:
private void Sheet1_Startup(object sender, System.EventArgs e)
{
//Is there a neater way to iterate through all Office Collections?
for (var i = 1; i <= Application.CommandBars.Count; i++)
{
var commandBar = Application.CommandBars[i];
//847 is a magical constant that any fule no has something to do with sheet deletion
var control = commandBar.FindControl(Id: 847, Recursive: true);
if (control != null) control.OnAction = null;
}
}
This code seems to do exactly nothing. You may ask "Hey, Gleno, why are you setting OnAction to null" , well I don't know what to set it to... The linked VBA solution attaches to Activate and Deactivate events, so there's more code where that came from.
Thanks in advance.

I had to do something very similar today. I would just disable the sheet delete buttons whenever your one "undeleteable" sheet is active. If there's a keyboard shortcut to delete a sheet, I can't find one. (If there is, you could disable that too.)
This would go in your ThisWorkbook class:
private void ThisWorkbook_Startup(object sender, System.EventArgs e)
{
this.SheetActivate += (sh) =>
{
this.ribbon.InvalidateBuiltinControl("SheetDelete");
};
}
public bool CanDeleteActiveSheet()
{
if (this.ActiveSheet == null)
return true;
// Replace Sheet1 with your sheet's CodeName
return ((Excel.Worksheet)this.ActiveSheet).CodeName != "Sheet1";
}
// Keep a local reference to the ribbon in your ThisWorkbook class
// so you can call InvalidateControl() from it.
Ribbon ribbon;
protected override IRibbonExtensibility CreateRibbonExtensibilityObject()
{
this.ribbon = new Ribbon();
return this.ribbon;
}
This would go in your ribbon code behind:
public void InvalidateBuiltinControl(string controlID)
{
this.ribbon.InvalidateControlMso(controlID);
}
public bool deleteButton_GetEnabled(IRibbonControl control)
{
return Globals.ThisWorkbook.CanDeleteActiveSheet();
}
And this would go in your ribbon xml:
<commands>
<command idMso="SheetDelete" getEnabled="deleteButton_GetEnabled" />
</commands>
I'm still a little leery of holding on to that ribbon reference in ThisWorkbook, but so far no one has mentioned a better way in the question I posted earlier. Hope this helps!

I'm having a similar problem where I know how to protect the worksheet but I need to turn protection on after the sheet has been populated with external data from a SQL connection. I cannot locate the correct event to do this.
This should help you put it in the startup event for the worksheet:
Me.Protect(password:="password", allowFiltering:=True, allowSorting:=True, allowUsingPivotTables:=True)

Related

Excel VSTO how to check if the Cancel button has been triggered from the dialog after the WorkbookBeforeClose event is fired?

I am currently developping an Excel VSTO addin and here is the code for WorkbookBeforeClose
private void App_WorkbookBeforeClose(Excel.Workbook Wb, ref bool Cancel)
{
bool isEnabled = false;
setRibbonControlState(ref isEnabled);
}
In this code I disable the ribbon if there is no workbook left opened. But if I press the Cancel button from the dialog after I tried to close Excel, the ribbon gets disabled anyway. But as the WorkbookBeforeClose event passed a Cancel parameter, I don't know how to set that parameter when I press the button, how do I check the dialog prompted for the button that has been triggered.
All cases I have seen so far implement a dialog in the body of the WorkbookBeforeClose handler, but I don't want to implement a custom dialog, I would like to use the one provided by default.
Thanks!
As of my VBA experience this
but I don't want to implement a custom
dialog, I would like to use the one provided by default.
is impossible, because that dialog appears just after the before_close event.
The only (known by me) way to manage this stuff - is to create own SaveChanges dialog, which is, by the way, very simple and not every user will notice the difference. And moreover, it will do the same job as default prompt.
The other thing you should pay attention to - is that there may be at least one invisible workbook. Even if you see such screen:
there is a chance that this.Application.Workbooks.Count will show you 1, instead of 0. This is due to the possibility that user has its own Personal.xlsb workbook, which is invisible, but nevertheless, is loaded with an Excel.Application. So in case you want to disable your ribbon properly - you should consider this as well.
Here is my example of this solution:
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
this.Application.WorkbookBeforeClose += ApplicationOnWorkbookBeforeClose;
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
// Catch the before close event
private void ApplicationOnWorkbookBeforeClose(Excel.Workbook wb, ref bool cancel)
{
if (!wb.Saved)
{
switch (MessageBox.Show(text:$"Do you want to save changes you made to '{this.Application.ActiveWorkbook.Name}'?",
caption:"Microsoft Excel",buttons:MessageBoxButtons.YesNoCancel, icon:MessageBoxIcon.Exclamation))
{
case DialogResult.Cancel: // case want to cancel - break the closing event
{
cancel = true;
return;
}
case DialogResult.Yes: // case user want to save wb - save wb
{
wb.Save();
break;
}
case DialogResult.No: // case user don't want to save wb - mark wb as saved to avoid the application messagebox to appear
{
wb.Saved = true;
break;
}
}
}
if (IsAnyWorkbookOpen())
{
// replace this with your code
MessageBox.Show("Some books will still be open, don't turn off the ribbon");
return;
}
// replace this with your code
MessageBox.Show("All books will be closed");
}
private bool IsAnyWorkbookOpen()
{
// check that remaining amount of open workbooks without the one being closed is greater that 2
if (this.Application.Workbooks.Count - 1 > 2)
{
return true;
}
// IF the count of workbooks is 2 one of them maybe a PERSONAL.xlsb
else if (this.Application.Workbooks.Count == 2)
{
foreach (Excel.Workbook wb in this.Application.Workbooks)
{
if (!wb.Name.Equals(this.Application.ActiveWorkbook.Name))
{
// In case when one of two open workbooks is Personal macro book you may assume that
// there will be no open workbooks for user to work directly
if (wb.Name.Equals("Personal.xlsb".ToUpper()))
{
return false;
}
}
}
// In case when NONE of two open workbooks is a Personal macro book
// there will be at least one open workbook for user to work directly
return true;
}
else
{
return true;
}
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
And last thing - if you disable your ribbon, but application will still be running - you will have to enable it again on workbook_activate event.
Note I'm just moving from VBA to VSTO - so any comments are highly appreciated.

VSTO Add-In CustomTaskPane opens itself after upgrading to Word 2016/2019

We've developed a Word VSTO Add-In for Word 2010 with CustomTaskPanes and MVVM Support through VSTOContrib.
After upgrading to Word 2016/2019 our CustomTaskPanes show up randomly without any action from the user.
It seems like that Word notices when a CustomTaskPane was used and wants to (re)open it automatically the next time.
For example, a CustomTaskPane opens while opening a new/exisitng document. Wouldn't be that bad, if it wouldn't glitch (open, close, open, close, ...) till it closes or stays open.
If the CustomTaskPane stays open, it is unusable because it has no DataContext that was loaded by our Add-In.
This Code in ThisAddIn creates/removes CustomTaskPanes:
public CustomTaskPane AddTaskPane(UserControl userControl, string title, Window owner)
{
return CustomTaskPanes.Add(userControl, title, owner);
}
public void RemoveTaskPane(CustomTaskPane taskPane)
{
if (taskPane == null)
return;
CustomTaskPanes.Remove(taskPane);
}
The RibbonViewModel (ViewModel per Document/Window) calls the Code like this.
The _addInHelper has events for creating/removing CustomTaskPanes to reach the ThisAddIn Code and returns the CustomTaskPane instance by callback. It also uses the IoC Container to resolve the view "CustomTaskPaneView".
// Gets called when a new Window opens or a new Document is opened
public override void Intialize(Document document)
{
// ...
CreateCustomTaskPane();
// ...
}
private void CreateCustomTaskPane()
{
if (_customTaskPane != null)
return;
_addInHelper.AddTaskPane("CustomTaskPaneView", "Custom headline", CurrentWindow, result =>
{
_customTaskPane = result;
});
if (_customTaskPane == null)
{
_log.Error(...);
return;
}
_customTaskPane.DockPositionRestrict = MsoCTPDockPositionRestrict.msoCTPDockPositionRestrictNoHorizontal;
_customTaskPane.Width = Settings.Default.TaskPaneWidth;
_customTaskPane.DockPosition = Settings.Default.TaskPanePosition;
// TaskPane height and width are saved seperately for DockPositionFloating
if (_customTaskPane.DockPosition != MsoCTPDockPosition.msoCTPDockPositionFloating)
{
// Set height and width for DockPositionFloating.
// If the user drags the TaskPane to Floating, it will have the correct size.
var oldDockPosition = _customTaskPane.DockPosition;
_customTaskPane.DockPosition = MsoCTPDockPosition.msoCTPDockPositionFloating;
_customTaskPane.Height = Settings.Default.TaskPaneHeight;
_customTaskPane.Width = Settings.Default.TaskPaneWidth;
_customTaskPane.DockPosition = oldDockPosition;
}
else
{
_customTaskPane.Height = Settings.Default.TaskPaneHeight;
_customTaskPane.Width = Settings.Default.TaskPaneWidth;
}
// Saving/updating settings in these
_customTaskPane.VisibleChanged += ContentControlsTaskPane_OnVisibleChanged;
_customTaskPane.DockPositionChanged += ContentControlsTaskPane_OnDockPositionChanged;
}
When closing the Window/Document, this code is called:
public override void Cleanup()
{
if (_customTaskPane != null)
{
SaveCustomTaskPaneProperties();
_contentControlsTaskPane.VisibleChanged -= ContentControlsTaskPane_OnVisibleChanged;
_contentControlsTaskPane.DockPositionChanged -= ContentControlsTaskPane_OnDockPositionChanged;
// Checks if the COM Object was cleaned up already
if (!_contentControlsTaskPane.IsDisposed())
{
// Tried to manually close the CustomTaskPane, but didn't help either
if (_contentControlsTaskPane.Visible)
_contentControlsTaskPane.Visible = false;
// Cleanup the CustomTaskPane ViewModel instance
var taskPaneViewModel = _contentControlsTaskPane.GetViewModel();
taskPaneViewModel?.Dispose();
_addInHelper.RemoveTaskPane(_contentControlsTaskPane);
}
}
}
This only happens while using Word 2016 and 2019 (we don't use 2013) and didn't happen with Word 2010 at all.
After upgrading the VSTO Project to VSTO Add-In 2013 and 2016 for testing purposes, it doesn't get better.
Example:
I didn't find any Word options that could cause this.
Any idea what this could cause and how to fix this / getting a workaround?
EDIT
Here is the updated code example WordTaskPanesBug
Steps to reproduce:
Start Word / run project
Click "Open" button
Click "New document" button
Click "New document" button, TaskPane gets opened (but won't glitch this time)
Also the CustomTaskPane glitches while closing the document in the example project, but not in our real project.
Old example gif
I added indices to indicate which task pane is being displayed, which showed that the task pane being added when you create a new document the second time is from the first document (the one that closes when you create a new document for the first time, probably because it's empty).
I think the problem you're running into is this one: Creating and managing custom task panes for multiple documents in a VSTO Word addin

Excel throws exception HRESULT: 0x800A03EC whilst user is still typing in a cell and cell values change

I'm creating an Excel addin in C# and I change some cell values with the addin button click. It works smoothly, however, I've noticed that if a user is trying to type something in ANY cell (not only the ones I'm changing values to), it will always throw HRESULT: 0x800A03EC exception.
My code is simple, for example I've tested this with only this piece of code:
private void buttonFoo_Click(object sender, RibbonControlEventArgs e)
{
var ws = Globals.ThisAddIn.Application.ActiveSheet;
ws.Range["A1"].Value = "bar";
}
And when I click on a random cell, type "foo", BUT I do not click anywhere on the worksheet, I do not finish writting "foo", instead I click on the buttonFoo whilst I'm still focused in my cell and it throws this exception.
This picture can explains what I'm trying to do, I'll be clicking on the Test button in the picture whilst still typing like this:
I thought this exception was happening if I was trying to type something in a specific cell and that specific cell I am also changing in my code. But that is not the case. I've tried selecting a cell that is not used, like this:
ws.Range["B1"].Select();
Does not help. I've tried sending ESC key to cancel the user typing:
excelApp.SendKeys("{ESCAPE}");
I tested this and it will actually send Escape, but it will still throw an exception. I'm completely out of ideas and have not found anything regarding this issue. I mean I could possibly surround everything with try/catch looking for this exception, but that's just terrible.. Anyone got an idea what to do?
try something like this instead to see if the application is busy:
bool XLAppBusy()
{
if (Globals.ThisAddIn.Application.Interactive == false)
{
return false;
}
try
{
Globals.ThisAddIn.Application.Interactive = false;
Globals.ThisAddIn.Application.Interactive = true;
}
catch
{
return true;
}
return false;
}
private void buttonFoo_Click(object sender, RibbonControlEventArgs e)
{
if (XLAppBusy() == false)
{
ws.Range["A1"].Value = "bar";
}
}

Excel Worksheet SelectionChange event doesnt get fired firsr time but works second time

I have a winform dialog in my excel add-in, which is popped up on click of a panel button. I have a selection change event added to worksheet. The event is not being fired first time. I have to close the dialog and the open it again, This time it will work. Am I missing something here, or it is bug with excel interop API?
Enviornment:Excel 2007, .NET 4.0, Interop runtime: v1.1.4322
Following is the code
public partial class CreateColumn : Form
{
public CreateColumn()
{
InitializeComponent();
Excel.Worksheet ws = Globals.ThisAddIn.Application.ActiveSheet;
//Bug: this event does not fire the first time.. works on second time.
ws.SelectionChange += new Excel.DocEvents_SelectionChangeEventHandler(ColRangeSelChange);
}
public void ColRangeSelChange(Excel.Range target)
{
System.Windows.Forms.MessageBox.Show(target.AddressLocal);
}}
This is how Create Column is being called
private void smartTemplateBtn_Click(object sender, EventArgs e)
{
Range SelectedRange = Globals.ThisAddIn.Application.Selection;
if (SelectedRange != null)
{
List<string> DataSetLabels = new List<string>();
foreach (Range cell in SelectedRange.Cells)
{
if (cell.Value2 != null && !cell.Value2.Equals(""))
{
if (!DataSetLabels.Contains(cell.Value2))
{
DataSetLabels.Add(cell.Value2);
}
}
}
if (DataSetLabels.Count > 0)
{
PopupCreateColumnDialog(DataSetLabels);
}
}
}
public void PopupCreateColumnDialog(List<string> DataSetLabels)
{
if (DataSetLabels.Count > 0)
{
CreateColumn colDialog = new CreateColumn();
colDialog.TopMost = true;
colDialog.Show();
}
}
After reading your comments, I think that this problem (and others which might potentially come up) derives from a non-too-good communication with Excel. Thus, this question will consist just in showing you a structure which shouldn't provoke any problem.
Right at the start of the application (or when you start to analyse the given Excel file), you have to define the Excel Object, the WorkBook and the Worksheet you will be dealing with (the first one). I will focus on the Worksheet by following your example:
Excel.Worksheet ws = Globals.ThisAddIn.Application.ActiveSheet;
ws.SelectionChange += new Excel.DocEvents_SelectionChangeEventHandler(ColRangeSelChange);
Where ColRangeSelChange is defined by:
public void ColRangeSelChange(Excel.Range target)
{
System.Windows.Forms.MessageBox.Show(target.AddressLocal);
}
While you deal with this spreadsheet you don't need to change this definition. Now the given method (ColRangeSelChange) is associated with the given event (ColRangeSelChange) and will be called every time, the event is triggered. If you keep redefining the Worksheet and the Event, you might get in coordination-related problems and weird situations might occur.
If you want to account for a different spreadsheets (via ActiveSheet again, or by any other mean), you would have to redo this process again (variable assignation and event assignation) with other variable or by keeping the same ones.
Summary: remove both Worksheet and Event definition from CreateColumn(). Put this right before starting to interact with the given worksheet (before smartTemplateBtn_Click). And make sure that you define events just once (at the start) and that you assign the given worksheet to a variable just once (at the start).

C# VSTO-Powerpoint-TaskPanes in separate windows.

I'm creating a VSTO for my company, and have ran across a interesting issue that I could use some help with. I will try to explain this to the best of my ability. I have the AddIn set up right now for it to create 2 customTaskPanes upon start up via Application.AfterNewPresentation events. And the ability to hide/show these based on user input from togglebuttons on the Ribbon.
Now when I fire up the first PowerPoint 2010 called "Presentation1" everything works great, I can show/hide the TaskPanes and everything inserts the way it should. Now then I open up a second template called "Presentation2"(to help keep things straight here) Everything works great again, I can show/hide the TaskPanes and everything inserts fine. If I go back to "Presentation1" the inserts and everything functions fine, but when I got to hide/show the TaskPanes it hides/shows them on "Presentation2". And if I create a "Presentation3" the same thing will happen but both "Presentation1" and "Presentation2" control "Presentation3" TaskPanes. And if I close the "Presentation2" and "Presentation3" the "Presentation1" buttons do not show/hide anything at all.
Code in the ThisAddIn
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
Application.AfterNewPresentation += new PowerPoint.EApplication_AfterNewPresentationEventHandler(Application_AfterNewPresentation);
}
private void Application_AfterNewPresentation(PowerPoint.Presentation Pres)
{
PowerPoint.Application app = Pres.Application;
PowerPoint.DocumentWindow docWin = null;
foreach (PowerPoint.DocumentWindow win in Globals.ThisAddIn.Application.Windows)
{
if (win.Presentation.Name == app.ActivePresentation.Name)
{
docWin = win;
}
}
this.myWebForm = new SearchWebForm();
this.myWebFormTaskPane = this.CustomTaskPanes.Add(myWebForm, "Search ",docWin);
this.myWebFormTaskPane.DockPosition = Office.MsoCTPDockPosition.msoCTPDockPositionRight;
this.myWebFormTaskPane.Width = 345;
this.myWebFormTaskPane.VisibleChanged += new EventHandler(WebFormTaskPane_VisibleChanged);
}
private void WebFormTaskPane_VisibleChanged(object sender, System.EventArgs e)
{
Globals.Ribbons.Ribbon1.searchButton.Checked = myWebFormTaskPane.Visible;
if (Globals.Ribbons.Ribbon1.searchButton.Checked == true)
{
myWebForm.SearchForm_Navigate();
}
}
And then this is in the ribbon
private void searchButton_Click(object sender, RibbonControlEventArgs e)
{
Globals.ThisAddIn.WebFormTaskPane.Visible = ((RibbonToggleButton)sender).Checked;
}
In PowerPoint 2007, custom task panes are shared across all presentation windows. If you want to have separate task panes assigned to each presentation you need to handle the corresponding events (WindowActivate, PresentationClose, etc.). You would also need to manage a list of all the task panes that you've created so you can show/hide the appropriate one. This is actually a well-known Outlook pattern frequently referred to in VSTO-world as InspectorWrappers - or in your case a DocumentWindowWrapper.
This has been changed for Powerpoint 2010 and now each taskpane is associated with a specific window. See this article.
Your error is that Globals.ThisAddIn.WebFormTaskPane does not necessarily correspond to the current presentations task pane - you need to lookup the proper task pane in your managed list (as mentioned above). When you create a new task pane (AfterNewPresentation), add it to your CustomTaskPane collection and provide a means of retrieving it.
public partial class ThisAddIn
{
private Dictionary<PowerPoint.DocumentWindow, DocumentWindowWrapper> pptWrappersValue =
new Dictionary<PowerPoint.DocumentWindow, DocumentWindowWrapper>();
}

Categories