Using Global Variables in VBA - excel

Just started using VBA within the past week, and am playing around with Sheets vs. Modules and defining variables. Here is the code I currently have:
Sheet1:
Dim writtenvoe As Boolean
Dim verbalvoe As Boolean
Dim voerequired As Boolean
Dim CA14 As Boolean
Dim CA15 As Boolean
Dim W2s As Boolean
Dim tranapp As Boolean
Module1:
Public Sub ImpDocs()
'Is written voe on file?
If Sheet1.CheckBox6.Value = True Or Sheet1.CheckBox8.Value = True Or Sheet1.CheckBox9.Value = True Or Sheet1.CheckBox10.Value = True Then writtenvoe = True Else writtenvoe = False
If Sheet1.CheckBox6.Value = True Then Sheet1.[O40] = "hello" Else Sheet1.[O40] = ""
If writtenvoe = True Then Sheet1.[O39] = "writtenvoe = true" Else Sheet1.[O39] = "writtenvoe=false"
'Is verbal voe on file?
If Sheet1.CheckBox11.Value = True Then verbalvoe = True
'Is a written VOE required?
If Sheet1.CheckBox16.Value = True Or Sheet1.CheckBox18.Value = True Or Not IsEmpty(Sheet1.[H33]) Then voerequired = True
If voerequired = True Then Sheet1.[J35] = "hello" Else Sheet1.[J35] = ""
'Are tax docs CA14 or CA15?
If Sheet1.CheckBox31.Value = True Or Sheet1.CheckBox7.Value = True Or Sheet1.CheckBox32.Value = True Or Sheet1.[H29].Value > 25 Then CA15 = True Else CA15 = False
If CA15 = False Then CA14 = True Else CA14 = False
'Are W-2's on file?
If Sheet1.CheckBox12.Value = True And Sheet1.CheckBox13.Value = True Then W2s = True
'Are 4506-T's on file?
If Sheet1.CheckBox60.Value = True And Sheet1.CheckBox61.Value = True Then tranapp = True
'Order Wage transcripts if W-2s and 4506-T not on file
If W2s = False And tranapp = False And CA14 = True Then Sheet4.fullCA14
End Sub
Also Module 1 (2nd Subroutine):
Public Sub test()
If writtenvoe = True Then Sheet1.[N37] = "Yes" Else Sheet1.[N37] = "No"
End Sub
I noticed that in this current format, Sub ImpDocs works well and the variables are determined correctly, but Sub test always comes back as false. However, if I put all of the code into module 1, everythis works as expected. It seems like only Sub test is affected from declaring variables in Sheet1 vs Module1. Is that true? If so, why?
Thank you.

Sheet1 is a Worksheet object, an instance of a class. So even if you declare everything Public on Sheet1, you won't be able to access them without first accessing the instance.
Dim is used for declaring local variables. It's also legal at module-level, but then it's equivalent to using Private to declare them: no variable declared with the Dim keyword is ever going to be publicly accessible - my recommendation is to use Dim for locals, and use Private/Public for module variables.
When you declare Public variables in a standard module (as opposed to a class module), you're effectively declaring global variables - each of which is accessible for both read and write, from anywhere in the project. If this sounds like a good idea, please take the time to research about "pros and cons of global variables" - regardless of the language, they're usually a recipe for disaster. You want your variables to be written by specific code only, and you want the scope of your variables to be as limited as possible.
Learn how to pass parameters to your procedures instead.
You're discovering the difference between standard modules and classes: pretty much everything around you (Application, Range, Sheet1, etc.) is an object. Objects are instances of a certain class (Excel.Application, Excel.Range, Excel.Worksheet, etc.), and each instance encapsulates its own state.
Standard modules are great for macro entry points, not so great for encapsulation - and encapsulation is one of the fundamental pillars of object-oriented programming (OOP).

This is a quirk on how VBA does its namespaces (https://en.wikipedia.org/wiki/Namespace). Back in Sheet1, you'll need to make writtenvoe a Public member of Sheet1:
Public writtenvoe As Boolean
Then, back in Module1, every time you want the writtenvoe of Sheet1, you must specify Sheet1.writtenvoe, e.g.:
'Is written voe on file?
If Sheet1.CheckBox6.Value = True Or Sheet1.CheckBox8.Value = True Or Sheet1.CheckBox9.Value = True Or Sheet1.CheckBox10.Value = True Then Sheet1.writtenvoe = True Else Sheet1.writtenvoe = False
When you moved writtenvoe out of Sheet1 and into Module1, you've effectively removed Sheet1.writtenvoe and created Module1.writtenvoe, thereby making something easily accessible by Module1. The namespaces of modules are very permissive, so anything declared Public inside a module is pretty much available anywhere - another module, any Sheet, any Class Module, etc.

Related

How to run a sub in a `Create` function and how to make a mock/stub/fake for a chart `Series`?

Preface
About 10 years ago I started refactoring and improving the ChartSeries class of John Walkenbach. Unfortunately it seems that the original it is not available any more online.
Following the Rubberduck Blog for quite some time now I try to improve my VBA skills. But in the past I only have written -- I guess the experts would call it -- "script-like god-procedures" (because of not knowing better). So I am pretty new to classes and especially interfaces and factories.
Actual Questions
I try to refactor the whole class by dividing it into multiple classes also using interfaces and than also adding unit tests. For just reading the parts of a formula it would be sufficient to get the Series.Formula and then do all the processing. So it would be nice to call the Run sub in the Create function. But everything I tried so far to do so failed. Thus, I currently running Run in all Get properties etc. (and test, if the formula changed and exit Run than. Is this possible and when yes, how?
Second, to add unit tests -- of course using rubberduck for them -- I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
And here a simplified version of the code.
Many thanks in advance for any help.
normal module
'#Folder("ChartSeries")
Option Explicit
Public Sub ExampleUsage()
Dim wks As Worksheet
Set wks = ThisWorkbook.Worksheets(1)
Dim crt As ChartObject
Set crt = wks.ChartObjects(1)
Dim srs As Series
Set srs = crt.Chart.SeriesCollection(3)
Dim MySeries As IChartSeries
Set MySeries = ChartSeries.Create(srs)
With MySeries
Debug.Print .XValues.FormulaPart
End With
End Sub
IChartSeries.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Function IsSeriesAccessible() As Boolean
End Function
Public Property Get FullFormula() As String
End Property
Public Property Get XValues() As ISeriesPart
End Property
'more properties ...
ChartSeries.cls
'#PredeclaredId
'#Exposed
'#Folder("ChartSeries")
Option Explicit
Implements IChartSeries
Private Type TChartSeries
Series As Series
FullSeriesFormula As String
OldFullSeriesFormula As String
IsSeriesAccessible As Boolean
SeriesParts(eElement.[_First] To eElement.[_Last]) As ISeriesPart
End Type
Private This As TChartSeries
Public Function Create(ByVal Value As Series) As IChartSeries
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New ChartSeries
.Series = Value
Set Create = .Self
End With
End Function
Public Property Get Self() As IChartSeries
Set Self = Me
End Property
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Private Function IChartSeries_IsSeriesAccessible() As Boolean
Call Run
IChartSeries_IsSeriesAccessible = This.IsSeriesAccessible
End Function
Private Property Get IChartSeries_FullFormula() As String
Call Run
IChartSeries_FullFormula = This.FullSeriesFormula
End Property
Private Property Get IChartSeries_XValues() As ISeriesPart
Call Run
Set IChartSeries_XValues = This.SeriesParts(eElement.eXValues)
End Property
'more properties ...
Private Sub Class_Initialize()
With This
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = New SeriesPart
Next
End With
End Sub
Private Sub Class_Terminate()
With This
Dim Element As LongPtr
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = Nothing
Next
End With
End Sub
Private Sub Run()
If Not GetFullSeriesFormula Then Exit Sub
If Not HasFormulaChanged Then Exit Sub
Call GetSeriesFormulaParts
End Sub
'(simplified version)
Private Function GetFullSeriesFormula() As Boolean
GetFullSeriesFormula = False
With This
'---
'dummy to make it work
.FullSeriesFormula = _
"=SERIES(Tabelle1!$B$2,Tabelle1!$A$3:$A$5,Tabelle1!$B$3:$B$5,1)"
'---
.OldFullSeriesFormula = .FullSeriesFormula
.FullSeriesFormula = .Series.Formula
End With
GetFullSeriesFormula = True
End Function
Private Function HasFormulaChanged() As Boolean
With This
HasFormulaChanged = (.OldFullSeriesFormula <> .FullSeriesFormula)
End With
End Function
Private Sub GetSeriesFormulaParts()
Dim MySeries As ISeriesFormulaParts
'(simplified version without check for Bubble Chart)
Set MySeries = SeriesFormulaParts.Create( _
This.FullSeriesFormula, _
False _
)
With MySeries
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last] - 1
This.SeriesParts(Element).FormulaPart = _
.PartSeriesFormula(Element)
Next
'---
'dummy which normally would be retrieved
'by 'MySeries.PartSeriesFormula(eElement.eXValues)'
This.SeriesParts(eElement.eXValues).FormulaPart = _
"Tabelle1!$A$3:$A$5"
'---
End With
Set MySeries = Nothing
End Sub
'more subs and functions ...
ISeriesPart.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Enum eEntryType
eNotSet = -1
[_First] = 0
eInaccessible = eEntryType.[_First]
eEmpty
eInteger
eString
eArray
eRange
[_Last] = eEntryType.eRange
End Enum
Public Property Get FormulaPart() As String
End Property
Public Property Let FormulaPart(ByVal Value As String)
End Property
Public Property Get EntryType() As eEntryType
End Property
Public Property Get Range() As Range
End Property
'more properties ...
SeriesPart.cls
'#PredeclaredId
'#Folder("ChartSeries")
'#ModuleDescription("A class to handle each part of the 'Series' string.")
Option Explicit
Implements ISeriesPart
Private Type TSeriesPart
FormulaPart As String
EntryType As eEntryType
Range As Range
RangeString As String
RangeSheet As String
RangeBook As String
RangePath As String
End Type
Private This As TSeriesPart
Private Property Get ISeriesPart_FormulaPart() As String
ISeriesPart_FormulaPart = This.FormulaPart
End Property
Private Property Let ISeriesPart_FormulaPart(ByVal Value As String)
This.FormulaPart = Value
Call Run
End Property
Private Property Get ISeriesPart_EntryType() As eEntryType
ISeriesPart_EntryType = This.EntryType
End Property
Private Property Get ISeriesPart_Range() As Range
With This
If .EntryType = eEntryType.eRange Then
Set ISeriesPart_Range = .Range
Else
' Call RaiseError
End If
End With
End Property
Private Property Set ISeriesPart_Range(ByVal Value As Range)
Set This.Range = Value
End Property
'more properties ...
Private Sub Class_Initialize()
This.EntryType = eEntryType.eNotSet
End Sub
Private Sub Run()
'- set 'EntryType'
'- If it is a range then find the range parts ...
End Sub
'a lot more subs and functions ...
ISeriesParts.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Enum eElement
[_First] = 1
eName = eElement.[_First]
eXValues
eYValues
ePlotOrder
eBubbleSizes
[_Last] = eElement.eBubbleSizes
End Enum
'#Description("fill me")
Public Property Get PartSeriesFormula(ByVal Element As eElement) As String
End Property
SeriesFormulaParts.cls
'#PredeclaredId
'#Exposed
'#Folder("ChartSeries")
Option Explicit
Implements ISeriesFormulaParts
Private Type TSeriesFormulaParts
FullSeriesFormula As String
IsSeriesInBubbleChart As Boolean
WasRunCalled As Boolean
SeriesFormula As String
RemainingFormulaPart(eElement.[_First] To eElement.[_Last]) As String
PartSeriesFormula(eElement.[_First] To eElement.[_Last]) As String
End Type
Private This As TSeriesFormulaParts
Public Function Create( _
ByVal FullSeriesFormula As String, _
ByVal IsSeriesInBubbleChart As Boolean _
) As ISeriesFormulaParts
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New SeriesFormulaParts
.FullSeriesFormula = FullSeriesFormula
.IsSeriesInBubbleChart = IsSeriesInBubbleChart
Set Create = .Self
End With
End Function
Public Property Get Self() As ISeriesFormulaParts
Set Self = Me
End Property
'#Description("Set the full series formula ('ChartSeries')")
Public Property Let FullSeriesFormula(ByVal Value As String)
This.FullSeriesFormula = Value
End Property
Public Property Let IsSeriesInBubbleChart(ByVal Value As Boolean)
This.IsSeriesInBubbleChart = Value
End Property
Private Property Get ISeriesFormulaParts_PartSeriesFormula(ByVal Element As eElement) As String
'NOTE: Instead of running 'Run' here, it would be better to run it in 'Create'
Call Run
ISeriesFormulaParts_PartSeriesFormula = This.PartSeriesFormula(Element)
End Property
'(replaced with a dummy)
Private Sub Run()
If This.WasRunCalled Then Exit Sub
'extract stuff from
This.WasRunCalled = True
End Sub
'a lot more subs and functions ...
You can already!
Public Function Create(ByVal Value As Series) As IChartSeries
With New ChartSeries <~ With block variable has access to members of the ChartSeries class
.Series = Value
Set Create = .Self
End With
End Function
...only, like the .Series and .Self properties, it has to be a Public member of the ChartSeries interface/class (the line is blurry in VBA, since every class has a default interface / is also an interface).
Idiomatic Object Assignment
A note about this property:
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Using a Property Let member to Set an object reference will work - but it isn't idiomatic VBA code anymore, as you can see in the .Create function:
.Series = Value
If we read this line without knowing about the nature of the property, this looks like any other value assignment. Only problem is, we're not assigning a value, but a reference - and reference assignments in VBA are normally made using a Set keyword. If we change the Let for a Set in the Series property definition, we would have to do this:
Set .Series = Value
And that would look much more readily like the reference assignment it is! Without it, there appears to be implicit let-coercion happening, and that makes it ambiguous code: VBA requires a Set keyword for reference assignments, because any given object can have a paraterless default property (e.g. how foo = Range("A1") implicitly assigns foo to the Value of the Range).
Caching & Responsibilities
Now, back to the Run method - if it's made Public on the ChartSeries class, but not exposed on the implemented IChartSeries interface, then it's a member that can only be invoked from 1) the ChartSeries default instance, or 2) any object variable that has a ChartSeries declared type. And since our "client code" is working off IChartSeries, we can guard against 1 and shrug off 2.
Note that the Call keyword is superfluous, and the Run method is really just pulling metadata from the encapsulated Series object, and caching it at instance level - I'd give it a name that sounds more like "refresh cached properties" than "run something".
Your hunch is a good one: Property Get should be a simple return function, without any side-effects. Invoking a method that scans an object and resets instance state in a Property Get accessor makes it side-effecting, which is a design smell - in theory.
If Run is invoked immediately after creation before the Create function returns the instance, then this Run method boils down to "parse the series and cache some metadata I'll reuse later", and there's nothing wrong with that: invoke it from Create, and remove it from the Property Get accessors.
The result is an object whose state is read-only and more robustly defined; the counterpart of that is that you now have an object whose state might be out of sync with the actual Excel Series object on the worksheet: if code (or the user) tweaks the Series object after the IChartSeries is initialized, the object and its state is stale.
One solution is to go out of your way to identify when a series is stale and make sure you keep the cache up-to-date.
Another solution would be to remove the problem altogether by no longer caching the state - that would mean one of two things:
Generating the object graph once on creation, effectively moving the caching responsibility to the caller: calling code gets a read-only "snapshot" to work with.
Generating a new object graph out of the series metadata, every time the calling code needs it: effectively, it moves the caching responsibility to the caller, which isn't a bad idea at all.
Making things read-only removes a lot of complexity! I'd go with the first option.
Overall, the code appears nice & clean (although it's unclear how much was scrubbed for this post) and you appear to have understood the factory method pattern leveraging the default instance and exposing a façade interface - kudos! The naming is overall pretty good (although "Run" sticks out IMO), and the objects look like they each have a clear, defined purpose. Good job!
Unit Testing
I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
Currently, you can't. When if/when this PR gets merged, you'll be able to mock Excel's interfaces (and much, much more) and write tests against your classes that inject a mock Excel.Series object that you can configure for your tests' purposes... but until then, this is where the wall is.
In the mean time, the best you can do is wrap it with your own interface, and stub it. In other words, wherever there's a seam between your code and Excel's object model, we slip an interface between the two: instead of taking in a Excel.Series object, you'd be taking in some ISeriesWrapper, and then the real code would be using an ExcelSeriesWrapper that works off an Excel.Series, and the test code might be using a StubSeriesWrapper whose properties return either hard-coded values, or values configured by tests: the code that works at the seam between the Excel library and your project, can't be tested - and we woulnd't want to anyway, because then we'd be testing Excel, not our own code.
You can see this in action in the example code for the next upcoming RD News article here; that article will discuss exactly this, using ADODB connections. The principle is the same: none of the 94 unit tests in that project ever open any actual connection, and yet with dependency injection and wrapper interfaces we're able to test every single bit of functionality, from opening a database connection to committing a transaction... without ever hitting an actual database.

Is it possible to Let and Get with different data types in VBA?

I'm currently working on an Excel VBA Add-In to process a CSV export from one system into the format the stakeholders would like to view it in. I'm fairly new to OOP, but I thought it might be possible to convert priority types within the Let code and have the data stored in the same format it will be output in.
I've written the following class:
'Class Module "Project"
Option Explicit
Private m_sPriority As String
Property Let Priority(lInput As Long)
Select Case lInput
Case 1
m_sPriority = "High"
Case 2
m_sPriority = "Medium"
Case 3
m_sPriority = "Low"
Case Else
m_sPriority = "No Priority"
End Select
End Property
Property Get Priority() As String
Priority = m_sPriority
End Property
And this is the module to test it:
'Standard Module
Option Explicit
Sub test()
Dim Project As Project
Set Project = New Project
Project.Priority = 1
Debug.Print Project.Priority
End Sub
I would expect to see "High" output to the console when running this.
Is there a way to accomplish this in VBA, or alternately, is my code malformed?
Yes, your code is malformed. For whatever it's worth, you can't do that in .NET either - the syntax makes it rather unambiguous, whether in C#:
private string _priority;
public string Priority
{
get { return _priority; }
private set { _priority = value; }
}
...or in VB:
Private _priority As String
Public Property Priority() As String
Get
Return _priority
End Get
Private Set(ByVal value As String)
_priority = value
End Set
End Property
A property is either a Long or a String, it can't be both. Get/Let/Set accessors must be consistent, in VBA as well.
You can cheat by losing type safety and early binding, by making your property a Variant... but that's not really a good way to get started with OOP ;-)
Public Enum PriorityLevel
NoPriority = 0
HighPriority
MediumPriority
LowPriority
End Enum
Private Type InstanceState
Priority As PriorityLevel
'...
End Type
Private this As InstanceState
Public Property Get Priority() As Variant
Priority = PriorityName(this.Priority)
End Property
Public Property Let Priority(ByVal value As Variant)
this.Priority = value
End Property
Private Function PriorityName(ByVal value As PriorityLevel) As String
Select Case value
Case HighPriority
PriorityName = "High"
Case MediumPriority
PriorityName = "Medium"
Case LowPriority
PriorityName= "Low"
Case Else
PriorityName= "Undefined"
End Select
End Function
While this works perfectly fine and looks pretty neat on the surface, consuming this class when you haven't written it is definitely going to be surprising: if you set (Let) a Long, you rightfully expect to Get a Long too. This class is begging for a PriorityName get-only property:
Public Property Get Priority() As PriorityLevel
Priority = this.Priority
End Property
Public Property Let Priority(ByVal value As PriorityLevel)
this.Priority = value
End Property
Public Property Get PriorityName() As String
Select Case this.Priority
Case HighPriority
PriorityName = "High"
Case MediumPriority
PriorityName = "Medium"
Case LowPriority
PriorityName = "Low"
Case Else
PriorityName = "Undefined"
End Select
End Property
Now, the benefits of early binding and type safety (whatever little of it you get to leverage in VBA) are immediately apparent: when you write code that assigns the Priority property of an object of this type, IntelliSense for the Enum type guides the writing of the expression:
Moreover, the Enum is abstracting the underlying numeric value, which becomes irrelevant: instead of a magic hard-coded 1, the code now says HighPriority. Also, the Get accessor is as simple as it gets, which is very good: a Get accessor should never raise any errors, so the simpler, the better.
Morale of the story: don't hack around type safety if you can help it, do everything you can to keep your code early-bound - avoid Object and Variant as much as possible; cast Object to a known class/interface everywhere you can.
For example prefer this:
Dim sheet As Worksheet
' Workbook.Worksheets(index) returns Object; casting it to Worksheet
Set sheet = ActiveWorkbook.Worksheets(1)
sheet.Range("A1").Value = 42
To this:
' "Range("A1").Value" is entirely late-bound. Beware of typos!
ActiveWorkbook.Worksheets(1).Range("A1").Value = 42

Replacing userform name with variable

I have the following function put in "Module1" determine the maximum value of a text box in user form, named "frmAddRecord2", that can be input.
txtLV and txtMaxLV are textboxes under "frmAddRecord2".
And txtMaxLV_Pass is a boolean variable put under frmAddRecord2 as well. At this stage, this function works fine.
Public Function txtLV_Max() As Long
With frmAddRecord2
If .txtLV.Value >= 99 Or Not .txtMaxLV_Pass Then
txtLV_Max = 109
Else
txtLV_Max = .txtMaxLV.Value - 1
End If
End With
End Function
As I will have "frmAddRecord1", "frmAddRecord2", "frmAddRecord3" ... etc, so I would like to call the following sub when frmAddRecord1 or frmAddRecord2
is activated.
Public Sub SetActiveUserForm(Optional UserFormName As String)
If UserFormName = "frmAddRecord1" Then
Set ActiveUserForm = frmAddRecord1
UserFormShown = True
ElseIf UserFormName = "frmAddRecord2" Then
Set ActiveUserForm = frmAddRecord2
UserFormShown = True
Else
Set ActiveUserForm = Nothing
UserFormShown = False
End If
End Sub
And I would like to restructure the function as:
Public Function txtLV_Max() As Long
With activeuserform
If .txtLV.Value >= 99 Or Not .txtMaxLV_Pass Then
txtLV_Max = 109
Else
txtLV_Max = .txtMaxLV.Value - 1
End If
End With
End Function
However, error occur at the Line "If .txtLV.Value >= 99 Or Not .txtMaxLV_Pass Then" . After testing, the txtMaxLV_Pass is found failed to be call after I restructured the function. And it work fine again if I moved txtMaxLV_Pass to be public under Module1.
But I would like to ask, if I want to keep the txtMaxLV_Pass under the userform, what should I changed in declare the userform variable. Please advice, I have studied this in web site and books, but still not able to tackle. Thank you for all your help.
The Global variable ActiveUserForm was given the type of Userform. However, to access UserForm properties from a Userform variable, it actually needs to be of type Object or Variant.

VBA: Can a set of "constants" be configured at runtime?

When writing macros for processing spreadsheets, I like to set constants at the top of the module, corrosponding to the various column numbers I'll have to use. I recently had a case where I would need to perform the exact same task on two slightly different file layouts. My solution was to turn the constants into variables, and call a configuration sub to set them depending on the file to be processed. The only thing I don't like about that solution is that what was constant, and protected against careless user (or developer(!)) tweaks to the code, is now in a variable.
Is there any way to make these configured values unchangeable in the main sub?
Original style:
Const iColUser = 1
Const iColColor = 2
Const iColPet = 3
Sub Main()
iColUser = 3 '<--This line will prevent sub from running.
New style:
Dim iColUser As Integer
Dim iColColor As Integer
Dim iColPet As Integer
Sub Config()
Select Case SpreadsheetType
Case a
iColUser = 1
iColColor = 2
iColPet = 3
Case b
iColUser = 3
iColColor = 2
iColPet = 1
End Select
End Sub
Sub Main()
iColUser = 2 '<--This line will run, and cause major damage.
Encapsulate them.
Add a class module, and make an abstraction over these. Abstract away the column numbering logic, exose Property Get accessors for your columns, and then another property with Get and Let accessors for the "mode", which determines the value returned by the properties.
Public Enum SpreadsheetType
A
B
End Enum
Private columnMode As SpreadsheetType
Public Property Get Mode() As SpreadsheetType
Mode = columnMode
End Property
Public Property Let Mode(ByVal value As SpreadsheetType)
columnMode = value
End Property
Public Property Get UserColumn() As Long
Select Case Mode
Case A
UserColumn = 1
Case B
UserColumn = 3
End Select
End Property
Public Property Get ColorColumn() As Long
Select Case Mode
Case A
ColorColumn = 2
Case B
ColorColumn = 2
End Select
End Property
Public Property Get PetColumn() As Long
Select Case Mode
Case A
PetColumn = 3
Case B
PetColumn = 1
End Select
End Property
Now to use it, you need an instance of the class. Assuming you called it Class1 (gosh don't do that!), using it would look like this:
Sub Test()
Dim sheetColumns As New Class1
sheetColumns.Mode = A
Debug.Print sheetColumns.UserColumn 'outputs 1
sheetColumns.Mode = B
Debug.Print sheetColumns.UserColumn 'outputs 3
End Sub
The code using this object can only ever read the values, not write to them - unless you implemented a Property Let accessor for the mutable values.

Resharper or CodeRush - global rename

Is there a way to rename all methods, properties etc. suggested by R#. I have code that I converted from java and all methods and properties are in the format like this "onBeforeInsertExpression" and I want them to follow camel casing that is common in .NET.
This question is also for CodeRush.
I needed the same functionality and couldn't find it. I considered writing an add-in to ReSharper using the Api but decided on a regular Visual Studio macro instead. This macro renames methods and private fields in the current document to the default ReSharper settings, but can easily be modified to iterate through all files in a project or solution.
Save this code as a .vb file and import it into your VS Macros.
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports EnvDTE90a
Imports EnvDTE100
Imports System.Diagnostics
Public Module RenameMembers
Enum NamingStyle
UpperCamelCase
LowerCamelCase
End Enum
Public Sub RenameMembers()
Try
'Iterate through all code elements in the open document
IterateCodeElements(ActiveDocument.ProjectItem.FileCodeModel.CodeElements)
Catch ex As System.Exception
End Try
End Sub
'Iterate through all the code elements in the provided element
Private Sub IterateCodeElements(ByVal colCodeElements As CodeElements)
Dim objCodeElement As EnvDTE.CodeElement
If Not (colCodeElements Is Nothing) Then
For Each objCodeElement In colCodeElements
Try
Dim element As CodeElement2 = CType(objCodeElement, CodeElement2)
If element.Kind = vsCMElement.vsCMElementVariable Then
RenameField(element)
ElseIf element.Kind = vsCMElement.vsCMElementFunction Then
'Rename the methods
ApplyNamingStyle(element, NamingStyle.UpperCamelCase)
ElseIf TypeOf objCodeElement Is EnvDTE.CodeNamespace Then
Dim objCodeNamespace = CType(objCodeElement, EnvDTE.CodeNamespace)
IterateCodeElements(objCodeNamespace.Members)
ElseIf TypeOf objCodeElement Is EnvDTE.CodeClass Then
Dim objCodeClass = CType(objCodeElement, EnvDTE.CodeClass)
IterateCodeElements(objCodeClass.Members)
End If
Catch
End Try
Next
End If
End Sub
'Rename the field members according to our code specifications
Private Sub RenameField(ByRef element As CodeElement2)
If element.Kind = vsCMElement.vsCMElementVariable Then
Dim field As EnvDTE.CodeVariable = CType(element, EnvDTE.CodeVariable)
If (field.Access = vsCMAccess.vsCMAccessPrivate) Then
'private static readonly
If (field.IsShared AndAlso field.IsConstant) Then
ApplyNamingStyle(element, NamingStyle.UpperCamelCase)
ElseIf (Not field.IsShared) Then
'private field (readonly but not static)
ApplyNamingStyle(element, NamingStyle.LowerCamelCase, "_")
Else
ApplyNamingStyle(element, NamingStyle.UpperCamelCase)
End If
Else
'if is public, the first letter should be made uppercase
ToUpperCamelCase(element)
End If
'if public or protected field, start with uppercase
End If
End Sub
Private Function ApplyNamingStyle(ByRef element As CodeElement2, ByVal style As NamingStyle, Optional ByVal prefix As String = "", Optional ByVal suffix As String = "")
Dim the_string As String = element.Name
If (Not the_string Is Nothing AndAlso the_string.Length > 2) Then
If (style = NamingStyle.LowerCamelCase) Then
ToLowerCamelCase(the_string)
ElseIf (style = NamingStyle.UpperCamelCase) Then
ToUpperCamelCase(the_string)
Else
'add additional styles here
End If
End If
AddPrefixOrSuffix(the_string, prefix, suffix)
If (Not element.Name.Equals(the_string)) Then
element.RenameSymbol(the_string)
End If
End Function
Private Function ToLowerCamelCase(ByRef the_string As String)
the_string = the_string.Substring(0, 1).ToLower() & the_string.Substring(1)
End Function
Private Function AddPrefixOrSuffix(ByRef the_string As String, Optional ByVal prefix As String = "", Optional ByVal suffix As String = "")
If (Not the_string.StartsWith(prefix)) Then
the_string = prefix + the_string
End If
If (Not the_string.EndsWith(suffix)) Then
the_string = the_string + suffix
End If
End Function
Private Function ToUpperCamelCase(ByRef the_string As String)
the_string = the_string.Substring(0, 1).ToUpper() & the_string.Substring(1)
End Function
End Module
No, unfortunately there isn't a way. Resharper's Code Cleanup / Reformat Code options work nicely for formatting, namepaces, etc, but will not do any automatic member renaming. You're kinda stuck doing a "Quick Fix" on each member. If you have a lot of them, this can be a pain...
CodeRush's approach to this kind of fix is more of an interactive process.
Which is to say you have to physically be in the location of the variable whose name you wish to change and you have to change each one individually.
That said, there is a very powerful engine under CodeRush called the DXCore, which can be used to create a very wide variety of functionality. Indeed it is this layer on which the whole of CodeRush and RefactoPro are built.
I have no doubt that it could be used to create the functionality you are after. However I doubt that you would use the existing rename technology. I will have to look into this a little further, but I am optimistic about being able to produce something.

Resources