I am in the process of continuously updating and improving a financial model built in Excel with VBA macros enabled that is in active use by multiple people. Primarily, these templates are used as budgets for different projects, so there are many created all the time while older budgets are re-visited.
I am the "keeper" of the template while the other users simply use the document. Whenever I need to push out an update to everyone, it creates an issue because they already have created the budget in an older version of the template and to re-create the budget in the new template would take an inordinate amount of time.
I have gotten around this problem on smaller-scale templates by naming ranges and then applying those named ranges to an old version and then using the named ranges to copy into the same named range in the new version of the template. However, this was done with individual lines of code to copy each named range.
Is there a way to aggregate a group of named ranges into a class so that Excel can just loop through all of the items in the class and copy the data rather than me needing to code out each line to perform a copy?
Here is a sample of the code that I am currently running:
Workbooks(WB_Active_Name).Sheets("Office Staff Input").Range("Office_Employee_Names").Value = Workbooks(WB_Secondary_Name).Sheets("Office Staff Input").Range("Office_Employee_Names").Value
Workbooks(WB_Active_Name).Sheets("Office Staff Input").Range("Office_Employee_Positions").Value = Workbooks(WB_Secondary_Name).Sheets("Office Staff Input").Range("Office_Employee_Positions").Value
Workbooks(WB_Active_Name).Sheets("Office Staff Input").Range("Office_Employee_Numbers").Value = Workbooks(WB_Secondary_Name).Sheets("Office Staff Input").Range("Office_Employee_Numbers").Value
Workbooks(WB_Active_Name).Sheets("Office Staff Input").Range("Office_Employee_Bonus_Sharing").Value = Workbooks(WB_Secondary_Name).Sheets("Office Staff Input").Range("Office_Employee_Bonus_Sharing").Value
There are dozen of more lines of code similar to this for each named range. Inside the template, the ranges refer to lists of names of staff, ID numbers, hours worked, etc. and they also span multiple sheets within the workbook with each range of different size.
I am wondering if there is some sort of class that I could place in front of each named range when I define it so that they are treated as a class together and can be looped through. For example:
Office_Employee_Names
becomes
GroupClass.Office_Employee_Names
Then the code could loop through everything in GroupClass
IF an MVC pattern would exist in VBA, then a named range could represent a View object.
I personally have some experience with an MV* pattern where Views also implement events (which are normally delegated to Controller objects).
The benefit of using this approach, is that you will start programming in a much more modular fashion.
I provide an example below:
The structure of an MV* implementation of a simple named range "persons" could have the following class structure:
cls_view_persons
cls_model_person
Imagine that cls_view_persons represents a view object, then this would mean that you have to instantiate it from a base sub, which will simply be:
dim view_persons as cls_view_persons
set cls_view_persons = new cls_view_persons
1. persons view (example)
The cls_view_persons class will have a property that defines the range of the class.
For example:
private pRange as new Range
You can define the private pRange property in the class constructor.
One of the things that makes Excel buggy, is the fact that you don't know in advance the size of the range, and the fact that mistakes happen, such as a range that was not properly cleared the last time.
This is why it is important to also define the following properties:
_oRange as Excel.Range
_sNamed_range as string;
_lNr_rows as long;
_iNr_cols as integer;
_iOffset_x as long;
_lOffset_y as integer;
oCollection as Collection (you can also use a dictionary for this)
Note: oCollection is an object that will consist of the different cls_view_persons instances;
and methods:
Init: class constructor: defining a default range object, or you could pass a range on creation time if you wish to do so;
get_named_range: returning a range object;
set_named_range: setting the range object property and creating the named range in the sheet object;
collection_to_array: converts the collection object into an array;
clear_range: clearing the range;
A render method that uses the previous three methods and writes data from the created array to the range in a single statement:
set pRange = vPersons
Where vPersons is an array, containing the different persons (see later).
(A "read" method that reads from the range would be useful as well).
2 person model (example)
A model represents the data logic of your application and is on itself, not necessarily related to one specific view. In this case it is, but a model (or Collection of models) is in principle independent).
A person model could be a class that defines the following properties:
firstname
lastname
address
country
Either these models are fetched from a database, from an Excel sheet (the latter being the worst scenario, which unfortunately happens the most) or any other source.
Whatever you do, you need to see that you end up with a Collection object that you can feed to the View object.
Once this is done, the View object should manage its own. All interpreting and rendering is delegated to this object from that moment on.
This means:
Verify the dimensions;
Clear the previous results;
Render the range on the screen (ie. setting the new dimensions, creating the named range in the sheet, converting the collection into an array, and writing the array to the screen).
You will see that this approach has many benefits in terms of:
Maintenance;
Less bugs;
Modular (and transferrable) / encapsulated properties / methods;
Dynamically adaptable;
You can read from any data source, if you only write an intermediary "translator" module.
I would create a new worksheet in your template that list all these named ranges; read the list from VBA and loop through them.
I often retrieve data from Names by:
Workbook.Names contains all Name Objects, and each Name Object its properties
Dim WBook As Workbook
Dim WName As Name
Set WBook = ActiveWorkbook
For Each WName In WBook.Names
Debug.Print WName.Name, WName.RefersToLocal
Next WName
Related
I am looking for a way to create unique Worsksheet identifier, which would take into account at least name and content. If any worksheet value is changed then the identifier would change as well. I have checked the GetHashCode() but for some reason each time I call this method on the same sheet it always changes. Any ideas how to create that identifier/hash code
Excel.Worksheet sheet2 = (Excel.Worksheet)Globals.ThisAddIn.Application.ActiveSheet;
var hash = sheet2.GetHashCode();
The CodeName of a worksheet is a unique identifier. VSTO uses the CodeName property for the internal name of the worksheet (probably Sheet1, Sheet2, etc.) when it creates a project with Worksheets in the Workbook.
In the VBA interface this property can be changed in the VBA IDE - it's the name used for the class module belonging to each Worksheet.
The property cannot be changed at run-time, only at design time. While the property may seem to reflect the caption for the worksheet (the name the user sees and can change in the UI), the two are not linked.
For more information, see:
https://msdn.microsoft.com/en-us/library/office/ff837552.aspx?f=255&MSPPError=-2147217396
http://www.cpearson.com/excel/RenameProblems.aspx
So I have an excel add-in that, amongst other things, contains a huge list of aliases. For example, Country names with their ISO codes, Countries with their continents, etc etc (so that I can easily know that Canada is in America or that Côte D'Ivoire's national language is French, etc). Currently, I have the xlam with all the relevant functions that check named ranges in another workbook.
So summarizing, I have 2 files:
macros.xls (has all the named ranges with things like Angola =
AGO, etc), and
my_functions.xlam (has the functions that I can call from
excel to get the Alias name). An example function is:
Function nti(v)
nti = WorksheetFunction.VLookup(Trim(v), Range("macros.xls!nti"), 2, 0)
End Function
Where nti is the function that I use and macros.xls!nti is the named range that I am referencing.
This all works great, but that means that this functionality requires two files, since an .xlam file can not be opened and edited in excel, only in VBA explorer.
Question: How can I have an easily modifiable lists and functions referring to these lists in one file? The functions should be available to other files I'm working on.
Disqualified solutions:
Save the my_functions.xlam as an .xls temporarily, copy all the named
ranges in, and then save it back as an .xlam. This would be annoying
to do every time I want to make a change to the named ranges (which
is fairly frequent).
Hardcode a bunch of 2 column arrays with all the
aliases into each function (thousands of lines long sometimes). PLEASE tell me there's a sexier way...
Thanks in advance!
I usually just toggle the IsAddin property of the XLAM from true to false, edit the worksheet and then toggle the property back again before saving.
I have a macro workbook with a number of worksheets that exist permanently, which are constantly cleared, updated, etc. Since they are referred to in various subroutines, I have made each corresponding worksheet object a pseudo-global variable in the following manner, for example for the "Main" sheet:
Function MAIN() As Worksheet
Set MAIN = ThisWorkbook.Sheets("Main")
End Function
By doing so, I can then refer to each sheet in the other subroutines, for example:
MAIN.Cells.ClearContents
I have also defined some pseudo-global constants which are located in a fixed place on the "Main" sheet in a similar way, for example:
Function NumLines() As Integer
NumLines = MAIN.Range("C3").Value
End Function
In this way, I use "NumLines" just like any variable throughout the code.
I expect that there is a more efficient way to manage globally accessed variables like these and was wondering, what would be a better way to accomplish this?
For reliable sheet reference I would suggest to use Sheet.CodeName Property. Each sheet has its unique CodeName which you could find in the place marked yellow on the picture below.
For quick reference to cell value I would suggest to use Range name. After you select you C3 cell you need to put unique name in the box marked yellow below. All Range names are unique in the workbook.
As a result you can use sheet and cell reference as presented below in each of your subroutines in your project.
Sub Test_Macro()
Debug.Print MAIN.Name '>> result: Sheet1
Debug.Print Range("CellC3").Value '>> result: 100
End Sub
I expect that there is a more efficient way to manage globally accessed variables like these and was wondering, what would be a better way to accomplish this?
When I use global variables in VBA, I do three things.
I always preface global variables with a g_ prefix. It seems often that a global variable in VBA is useful. But I've also spent far too long trying to track down "what variables are global or not?" in other people's code. Keeping a very clear naming convention will save you and whoever looks at your code a TON of hassle in the future.
This is even more important if you are less experienced as a developer. Avoiding globals is hard in VBA, and the less experience you have, the more likely it is you will use globals. For others to help or maintain the code this becomes so important.
If you are going to be using even a small number of global variables, you must use Option Explicit unless you want to cause nightmares in maintaining code. It's hard enough to track down these errors when you wrote code let alone months or years later.
I always create a module which is called "GlobalVariables" or something similar. That module contains all of the global declarations in one location. For larger code bases this can become longer but it has always paid off for me because I know exactly where all my globals are defined. None of the "which file is this variable actually being defined in?" game.
Just an unrelated note, too, in your first example - I would use the code name rather than that function. Each VBA worksheet has a sheet name ("Main" in your case) as well as a codename, which you can set in VBA and remains the same. This prevents users from changing the name of "Main" and breaking code.
You can also refer directly to them similar to how you are using MAIN.Cells. KazJaw has a good example of this.
I have a spreadsheet where the user inputs various details on an inputs page and then presses a calculate button to get what they want. The inputs are strings, numbers and dates.
I want to save the inputs for each calculation for the user so that at a later date they could enter the calc id and not have to renter the inputs.
One simple way I thought of doing this was to copy the inputs when the calculation is run to another sheet with the inputs in a column with the calc id. Then just save future inputs in a separate column and lookup the correct column to retrieve the inputs at a later date.
I read this question - What are the benefits of using Classes in VBA? and thought it would be good to make a class called CalculationInputs that had all the details stored in one object. This may be overkill for what I need but i wanted to ask how other people would solve this simple task.
You can use Names to define variables within the scope of a workbook or worksheet. Typically these are used to define ranges, and more specifically dynamic ranges, but they can also be used to store static/constant values.
To create a Name manually, from the Formula ribbon, Names Manager:
Click on the "New" button, and then give it a meaningful name:
Make sure you put ="" in the "Refers To" field, if you leave it blank, the name will not be created.
Then when you press OK, or any time you go to the Names manager, you will see a list of all available Names in the workbook.
You can edit these through the Names manager, which is probably tedious, or you can easily use VBA and inputs to control them, for example:
Sub Test()
ActiveWorkbook.Names("MyAddress").RefersTo = "734 Evergreen Terrace"
End Sub
You could do something like this to capture the value, our use other macros or user firm code to assign the value to the Name.
Activeworkbook.Names("MyAddress").RefersTo = _
Application.Inputbox("please enter your address")
Etc.
If you run this, and then review the Names manager, you'll see the value has been updated:
In VBE, you can refer to the name like:
Debug.Print ActiveWorkbook.Names("MyAddress").Value '# Prints in the immediate pane
Range("A1") = ActiveWorkbook.Names("MyAddress").Value
These can also be accessed (read) from the worksheet, like:
I have a report that needs to process the data that it get from SQL before show it.
For that, I have a custom code, and a Dictionary where I push all the processed data.
My problem is that if I save the dictionary in a report variable when I export the report to Word that variable seems to be cleaned.
What is the lifecycle of the reports variables? What is the most convenient way of saving an object during the report life.
Thanks!
I have been playing around with custom code for a about 6 weeks so I can answer some parts of the question of variable lifecycle in SSRS 2008 R2.
I have report that uses a Dictionary to store totals, allows me provide some specialist subtotals for financial stuff. I have something you can check (as I can't yet comment on things).
Have you declared the variable as 'shared', this is a custom code specific keyword that doesn't translate into VB.net. It ensures the variable lives to the next page, I tested this to Excel and word seemed to work fine transferring over the variable's data.
There is a trade off however under SSRS "report on demand" engine (on web, but not on BIDS) it holds the variable and doesn't garbage collect until the cache itself is cleared. I wrote some more custom code to indicate when my parameters changed and clears the variable.
Code;
Public Shared Dim Totals As New System.Collections.Generic.Dictionary(Of String, Decimal)
Public Function WipeKeys() as Decimal 'Clear Data from Dictionary (this will clear the cached object as well)
Totals.Clear()
Return 0D
End Function
I will also to recommend overwrite the key where ever possible to ensure reduction of addition loops.
Regards,