Trying to work with a protected workbook (i have access to the password) with a VBA project (1 UserForm, 5 Class Modules, 1 Standard Module)
I'm really confused about how to properly work with protected sheets in VBA.
I've tried doing an
ActiveSheet.Unprotect PWD
followed after a method call with
ActiveSheet.protect PWD
Also tried
ActiveSheet.protect PWD, UserInterfaceOnly:=True
from the Workbook_open sub
The issues is at random points its like the protection reverts to being protected and the parts of my VBA project that are making changes to the actual spreadsheet freak out.
If i pause the debugger go over and manually unprotect the sheet and hit continue things go off without a hitch
So what's going on here
Is it not enough to unprotect the sheet at the beginning of a method that calls other methods that make changes to the sheet and reportected it at the end?
or do I literally have to prefix and postfix
ActiveSheet.Unprotect PWD
ActiveSheet.Protect PWD
around EVERY single line that changes the sheet data?
Because it seems like the protection cares nothing about anything my VBA project does internally ONLY when I try to do subsequent things with the worksheet
I can post code later today when I get home if its needed
Is it not enough to unprotect the sheet at the beginning of a method that calls other methods that make changes to the sheet and reportected it at the end?
This is why global state is such a pain in the neck. As far as your VBA project is concerned, sheet protection state is global. So if you do:
Public Sub Procedure1()
Sheet1.Unprotect PWD
Procedure2
'do stuff on Sheet1
Sheet1.Protect PWD
End Sub
If Procedure2 re-protects Sheet1 before it exits, then Sheet1 is protected again when Procedure1 resumes to do stuff on Sheet1, and as you're experiencing, it doesn't like it much.
So yes, you need to be sure that the sheet isn't protected before you try to modify it.
or do I literally have to prefix and postfix [...] around EVERY single line that changes the sheet data?
You don't. I mean, you do, but you don't, if you manage your global state in a sane way. If anyone anywhere at any time can go in and re-protect the sheet you just unprotected, your swear jar can fill up rather quickly.
Similarly, Application.Calculation is global; yet if anyone anywhere can set it back to xlCalculationAutomatic after you carefully made it xlCalculationManual in an effort to "improve performance" (same goes for Application.ScreenUpdating), then you'll ultimately find yourself triggering recalculations and introduce very noticeable lags instead.
Global state is nice (hey I can access it from anywhere!), but also double-edged. If you don't structure things properly, global state quickly spagghettifies and becomes a tangled mess of inefficient back-and-forth toggles that you don't need, or want.
The solution is to set yourself up for success, and layer your code properly. Have all the sheet-protection-toggling code in one place, and constrain yourself to only ever invoke that logic from one single layer - everything underneath isn't concerned with sheet protection, it's none of its business. If the sheet it means to work with is protected, then it's not its problem - code in the upper layer has a bug.
In the above example, Procedure2 would be layer 2, and it shouldn't be allowed to toggle sheet protection at all. Let Procedure1 be responsible for that, and move the do stuff on Sheet1 part to some new Procedure3, that 's just as carelessly assuming sheet protection is already handled.
You can even encapsulate the toggling in a class, e.g. Sheet1Protection:
Option Explicit
Private Sub Class_Initialize()
Sheet1.Unprotect PWD
End Sub
Private Sub Class_Terminate()
Sheet1.Protect PWD
End Sub
And now you can do this:
Public Sub Procedure1
With New Sheet1Protection 'unprotects sheet1
Procedure2
Procedure3
End With ' object terminates, sheet1 is protected again
End Sub
Notice that sheet protection is being toggled at the very last minute, when it's needed, and wraps all operations that need to run with the unprotected sheet.
With tooling such as Rubberduck (an OSS add-in project for the VBE, which I manage & contribute to) you can easily locate all the places where sheet protection is being toggled outside of Sheet1Protection, and remove them.
Related
I have a worksheet containing a column of numbers. The column is formatted with a background color, number formats, etc. The column is unlocked. I protect the sheet manually by right-clicking on the tab and selecting Protect. In the Protect Sheet dialog, 'format cells' is unchecked. I interpret to mean that the user should not be able to format cells. Yet when the user pastes into the column, formats are pasted along with values.
If I protect the sheet in VBA using
sh.Protect UserInterfaceOnly:=True
I get the same result: formats are pasted. I do not specify AllowFormattingCells:=False because the default is False.
I have seen posts suggesting that formats can be restored by copying and pasting them from a shadow area. I have used this solution before I started protecting worksheets and found it overly complex. I had hoped this was something protection would handle. If there is a way to handle it, I'd like to do it in VBA.
This question is a little old, but I had the same one, and so have many people in the past. With a bit of browsing I came up with a solution which seems quite clean and appears to work. Am I missing something?
Private Sub Worksheet_Change(ByVal rngTarget As Range)
Dim vPaste As Variant
With Application.CommandBars("Standard").Controls("&Undo")
If Not .Enabled Then Exit Sub
If .ListCount < 1 Then Exit Sub
If .List(1) <> "Paste" Then Exit Sub
End With
vPaste = rngTarget.Value2
On Error Resume Next
Application.EnableEvents = False
Application.Undo
rngTarget.Value2 = vPaste
Application.EnableEvents = True
On Error GoTo 0
End Sub
This could go in Workbook_SheetChange, but not if you already have code in Worksheet_Change, because the worksheet event handler gets called before the workbook event handler. But this code can go in a module to keep things tidy.
There is no built-in protection option you can use to achieve your desired result.
The only thing that works in this case is the clunky workaround that you mention, i.e. use a Worksheet_Change event that ensures the correct format after a cell has been modified.
Since there are many different ways to paste content, i.e. via various menus, ribbon commands, keyboard shortcuts, etc., any VBA solution that tries to intercept pasting will become very complex, much more complex than the change event that restores the format to its original state.
Another option might be user education and training (so they know to paste values only), although user behaviour may be the toughest element to change in the whole scenario.
I have a workbook with many worksheets. Some of them must be protected, and some of them unlocked. The tricky part is that a userform created using Excel VBA will be used to add data to one of the protected worksheets, and the code fails when said worksheet is protected because it is supposed to add rows to a table to save the data.
I tried using the following code to fix this:
Sheets("Sales").Protect Password:="123", AllowInsertingRows:=True, UserInterFaceOnly:=True
Although after running it I can manually add rows through the user interface, it seems that the property I am trying to use only works that way, VBA code still can not and new rows to the protected worksheet.
So, the workaround I thought of is to unprotect the worksheet when the userform is initialized, like this:
Private Sub UserForm_Initialize()
Sheets("Sales").Unprotect "123"
End Sub
This way the userform can save all the data without any issues. However, my question is how to properly protected it again. There are two ways of closing the userform, one is with a cancel button that just runs the Unload Me piece of code, and the second one is using the X on the top right corner of the form. I would like to keep those two options available, but also make sure that the worksheet is protected again before the user can try to edit it (it is imposible to select anything with the userform open).
What I am thinking would be the best option is the following code:
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Sheets("Sales").Protect Password:="123"
End Sub
After testing it I believe it works, but I am not sure if this is the proper way of doing this. Maybe there is a specific situation that I am not considering that would end up with the user being able to edit the worksheet that is supposed to be protected. Could please let my know if there are any flaws in my code?
I have a chart that I want user not to delete. Yet I'd like to give him option to move or resize it if he wants to, so I think, that option locked in properties is not appropriate here (I use secure worksheet too).
Is there any possibility here to make code that works when user clicks Del on the keyboard, then some msgBox with information with error, that this chart is not deletable appears and then discard this action of removing?
It is impossible to prevent Chart deletion, while simultaneously enabling it to be moved
There are two following options, which *sorta* achieve what you want:
Enable Sheet Protection (Review -> Protect Sheet) which will achieve the following:
prevent the Chart from being deleted
unfortunately also prevent chart from being moved
Use Application.OnKey to detect press of Delete
Private Sub Worksheet_Activate()
Application.OnKey "{DELETE}", "PreventDeletion"
End Sub
Private Sub PreventDeletion()
MsgBox ("Deletion not allowed in this worksheet")
End Sub
this allows the chart to be moved
unfortunately only creates an illusion of delete protection. If user uses mouse2 and selects the delete option, it will still be available to him. This only interrupts pressing the Delete
also this renders Delete obsolete for entire Sheet, which can be annoying (for the users)
Neither option is ideal, though personally I prefer the first one, as preventing usage of Delete altogether is bound to annoy your users.
So I am currently studying SQL Server but right now I am just working a standard office admin job while I'm studying.
I never really made macro's before and little knowledge on VB but decided to design a macro for work to make things a bit easier for my team.
The macro just very simply allows the user to enter data, stats etc and gives the percentage or average statistic resulting in a total letting the user know if the statistics have been hit that day, week, month etc.
It works well but I would like to add a "SUBMIT" button that when a user clicked it would send the data they have entered in specified cells to myself. I am not sure how to go about it, If needed I don't have access to systems like SQL, Visual Studio etc in work as said just basic admin job at the moment.
Would It need to be submitted as a CSV? or could it be submitted from the user's sheet straight onto another macro I have designed giving the results for the whole team? As said I am totally new to this idea.
Cheers Guys.
Awright, according to what you may need in a very simple approach, the first thing you need to do it's to know the cells where they're going to enter info (care with ranges ), let's assume for this example that whe only had one data entered in the first cell of the team worksheet. So, create a button called 'button1' or as you wish and on the click event use this code :
Private Sub button1_click()
Teamsheet.Cells(row,column) = Yoursheet.Cells(destinyrow,destinycolumn)
End Sub
That would copy the value from one sheet to another, now, if you had you sheet locked via password, you must unlock it before doing that,then lock it again so code would be like this :
Private Sub button1_click()
On Error Resume Next
yoursheet.unprotect password:="yourpassword"
Teamsheet.Cells(row,column) = Yoursheet.Cells(destinyrow,destinycolumn)
On Error Resume Next
yoursheet.PROTECT password:="yourpassword"
End Sub
I clarify that this is a very simple approach, so, if you're using specific cells you can copy one by one and this would do (so you can make anny calculation son your admin sheet), but when you're copying ranges should be like this :
Teamsheet.Range("A1:D3").Value = yoursheet.Range("A1:D3").Value
Also, always consider how they enter this data you need.
UPDATE :
Let's say you have a team workbook and yours is admin_workbook, concept it's similar. This code will do what you need but both workbooks should be at the same folder or path :
Private Sub button1_click()
Var_data = Teamsheet.Cells(row,column)
Application.ScreenUpdating = False
Workbooks.Open Filename:=ThisWorkbook.Path & "\admin_workbook.xls"
ThisWorkbook.Activate
Admin_sheet.Cells(destinyrow,destinycolumn) = var_data
Workbooks("admin_workbook.xls").Close SaveChanges:=True
Application.ScreenUpdating = True
End Sub
First you capture data on a var, then you open your admin book, put the data on the cell you want and close that workbook saving changes (you decide if you keep this line or mantain the workbook open and save manually). Also, Application.screenupdating it's a line that helps your screen doesn't flick when changing between workbooks.
Hope it helps friend !
Good day,
I am using Excel 2013 and I would like to hide and unhide my Sheets as I work with them. I spent some time Googling around and found plenty of ancient posts on forums about adding VBA to modules, but that's not quite what I'm looking for.
On a main page where I spend most of my time using data, I have a button that shows a UserForm with a list of sheets in a ListBox. I choose the Sheet from the ListBox, hit OK, and it runs the following;
Private Sub OKButton_Click()
ThisWorkbook.Sheets(JobListBox.value).Visible = xlSheetVisible
ThisWorkbook.Sheets(JobListBox.value).Activate
Unload Me
End Sub
I would like it so when I have my new sheets created via VBA, I can populate the sheet with the following subroutine;
Private Sub Worksheet_Deactivate()
Me.Visible = xlSheetVeryHidden
End Sub
If anyone can let me know how I can make a Subroutine to insert this code into my sheets, I would greatly appreciate it.
PS: My fallback method is to, of course, just copy/paste the code manually... But I would prefer to automate it if possible.
Instead of adding the same code to each sheet, since they are all inside the workbook and you are really trying to execute a hide once any sheet in the workbook is deactivated, put this in the code for ThisWorkbook.
Private Sub Workbook_SheetDeactivate(ByVal Sh As Object)
Sh.Visible = xlSheetVeryHidden
End Sub
You might be able to use more workbook events with your type of project.
Here is a list of the workbook events.
If you want to exclude your main page from this, you can modify this by adding an IF statement:
Private Sub Workbook_SheetDeactivate(ByVal Sh As Object)
IF Sh.Name <> "Main" Then
Sh.Visible = xlSheetVeryHidden
End If
End Sub
The main line of thinking being that if you have to put the same code into more than one object, let alone ALL of them, you are repeating yourself.
Check out the concept of DRY, or "Don't Repeat Yourself", unless you like it WET, "We Enjoy Typing". or "Write Everything Twice". Even if it's just going to be created programmatically, a chunk of code shouldn't have to exist in all your sheets exactly the same when you can have one piece of code have an incoming argument that is a worksheet.
This way, if you have to make a change to its behavior, you do it once. It's easily testable and less to keep track of or modify later.
So if you find yourself having to use the same code over and over, look to the parent object and try to find a way to pass the changing object or variable through as an argument to a singular piece of code, or module.
Also, this is probably why you aren't finding any results on inserting the same code into every sheet. It's not a good practice
Article on DRY