I have a strange problem with two of my Excel Tables residing on two different worksheets in my project. I am using VSTO but VBA shows the same result: an empty table's row count has 0 rows in one case and 1 row (I presume the insert row) in another case.
The Setup
Two worksheets: Sheet1, Sheet2
Two corresponding named Excel Tables: Sheet1Table, Sheet2Table
Both tables are empty, i.e. they have one empty row, which is insert row that cannot be deleted.
I run the following code to determine the number of data rows (i.e. excluding the header row):
Microsoft.Office.Tools.Excel.ListObject sheet1Table = Globals.Sheet1.Sheet1Table;
int numberOfListRows1 = sheet1Table.ListRows.Count;
and
Microsoft.Office.Tools.Excel.ListObject sheet2Table = Globals.Sheet2.Sheet2Table;
int numberOfListRows2 = sheet2Table.ListRows.Count;
The result is that numberOfListRows1 is 1 and numberOfListRows2 is 0 although the result (whichever is correct) should be the same. I compared the table and worksheet properties, as well as the source files in Visual Studio, and I could not spot any differences. Any idea what I should be looking for (and which result is the correct one)?
In VBA I used these subs to test your case:
Sub Sheet1TableRowsCount()
Dim numberOfListRows1 As Integer
Dim sheet1Table As ListObject
Set sheet1Table = Sheet1.ListObjects("Sheet1Table")
numberOfListRows1 = sheet1Table.ListRows.Count
Set sheet1Table = Nothing
End Sub
Sub Sheet2TableRowsCount()
Dim numberOfListRows2 As Integer
Dim sheet2Table As ListObject
Set sheet2Table = Sheet2.ListObjects("Sheet2Table")
numberOfListRows2 = sheet2Table.ListRows.Count
Set sheet2Table = Nothing
End Sub
These are the tables:
When these tables are created (incorrectly) by selecting the headers and one empty row and then formatting the selection as a table, they have one empty row. This row has no content, COUNTA across the row is 0, and it appears like the insert row that cannot be deleted. It is however a valid data row, so ListRows.Count will get the value of 1.
Similarly, if you fill in some data and manually delete those entered values (using Delete) so that your tables would look like at the beginning, the results will still be 1.
If you delete rows manually or programmatically (using something like Sheet1.Range("Sheet1Table").Rows("1").Delete), the ListRows.Count will then yield the value of 0.
The solution to determining the actual count of data rows is to check ListRows.Count, and if the result is 1 - delete the row after checking if it does not contain actual values. This whole situation can be avoided if an empty table is created by selecting the header row only before clicking Format as Table. The insert row is then created automatically and not counted as a data row (the result of ListRows.Count in this case is 0).
Related
I've been working on a problem for a few days now and have almost arrived at the solution. I have been currently working on an excel application where I have a macro, operated by a button click, which resets the numerical values within certain cells in a table.
Within this table there are 3 columns; "Quantity Fitted (n)", "Quantity Required (m)" and "Lock Configuration".
What I need to happen, is when the button is clicked, the numerical values for each line within the "Quantity Fitted (n)" column are reset to match the values displayed within the "Quantity Required (m)" column of the same line.
However, if the cell value within the "Lock Configuration" column of that row is set to "locked", I want the "Quantity Fitted (n)" value to remain unchanged after the button click.
I hope this makes sense! Here's my current attempt to code this:
Dim x As Long
'Set reference to your table. Have hard-coded the sheet name and table name in.
Dim MyTable As ListObject
Set MyTable = Worksheets("System").ListObjects("Table_System")
'These will be the numerical index numbers of the columns in your table.
'This assumes your "Locked" column is in the table and has the header "Locked".
Dim Required As Long, Fitted As Long, Locked As Long
Required = MyTable.ListColumns("Quantity Required (m)").Index
Fitted = MyTable.ListColumns("Quantity Fitted (n)").Index
Locked = MyTable.ListColumns("Lock Configuration").Index
'Look at each row in the table. Am using `x` rather than `Each Cell`
'as the row number of the cell may differ from the row location in the table
'e.g. If your table starts on row 2, then first row after the header is row 3 - row 3 as Cell, but row 1 in table.
For x = 1 To MyTable.Range.Rows.Count
'If Locked column doesn't say "Locked" then copy value from
'Fitted to Required columns, otherwise do nothing.
If MyTable.DataBodyRange.Cells(x, Locked) <> "Locked" Then
MyTable.DataBodyRange.Cells(x, Fitted) = MyTable.DataBodyRange.Cells(x, Required)
Else
'Do Nothing.
'The ELSE and comment aren't really needed - just here to show nothing happens.
End If
Next x
End Sub
This achieves the aim, however the code breaks down due to an incorrect count of the rows within the table. There are 3 rows within my table, and by typing the formula directly into the excel sheet of =Rows(Table_System) the value of 3 is returned.
However, within my code, I can see by declaring a new variable Dim y As Long and setting it so y = MyTable.Range.Rows.Count that the value for y being returned is 5.
Similarly, if I increased the rows in my table by 1, the excel formula updates its solution to 4. However, the y value within my code now displays 6. It's always +2 ahead? Why would this be?
Thank you!
MyTable.Range
includes column header row and any footer (eg totals) row you might have
MyTable.DataBodyRange
includes only the data rows. So this should work:
For x = 1 To MyTable.DataBodyRange.Rows.Count
I would like to find the text "Currency" in columns A or B, store all the currencies listed under Currency. Same process for Amount (Can be in an column)
Store values in an array. Then paste in Output Sheet. The currencies will already be listed in Output sheet in 1st row of the sheet. However if it is a new currency then the code should find last used cell in row 1 and add it. The value of Amount should be added to Output sheet against the currency and ID number also copied from the Source sheet.
I have some code.
Public Sub loopRow()
Dim curArray As Variant
Dim listarray As Variant
Dim cnt As Long
'Find Currency
Dim rgFound As Range
Set rgFound = Range("A:B").Find("Currency")
'Find last used row
curArray = Cells(rgFound.Address).End(xlUp).Row
'Transpose list of currecny from the row down from the word Currency that it has found
listarray = Application.Transpose(Cells(Rows, curArray).End(xlUp)).Row
For cnt = LBound(curArray) To UBound(curArray)
curArray(cnt) = curArray(cnt)
Next cnt
For cnt = LBound(curArray) To UBound(curArray)
'Debug.Print curArray(cnt)
'Copy and paste into Sheet under the correct curreny, if new currency then add this in row A
Next cnt
End Sub
Whilst you need to understand your question is unanswerable as is, I'll do my best to help.
The problem we have is not seeing the source sheet the way you do, as we can't see it at all. You say you have the word Currency in columns A or B or both, and an ID column somewhere, and Amount values everywhere. That's tricky source data. If as is more likely, the ID is in a specific column and the amounts are in a set of columns, then we'd have a chance.
Your question outlines the basic steps you'd want to take pretty well, so you're off to a good start.
However you can do all of the work without VBA, certainly if I'm right about the Source data. Create yourself a working sheet, or multiple working sheets. Definitely one to sort out the full list of currencies. Grab a copy of columns A and B (by formulae) and then have the working sheet go through line by line and use logic to build the list. Spreadsheets are great at this.
Once you have the list, use it as row headers on your Output sheet and use sumifs to get the values. I am not sure how the IDs would fit in, but if they were to be your row headings, then do the same as the above to get the list of unique ids and link them into your Output page in column A. Your sumifs can handle that.
That will hardly tell you all you need to know, but if you work it out you'll have learned a lot about Excel and when you need to go into VBA.
If you'd rather do it with VBA, break down each step until it works, and then go onto the next one.
And if you want more help, paste your data in here. Anonymise it first if you need to.
I've run into a bit of a road block. I get a .PDF output from an accounting program and copy/paste the data into excel, then convert text to columns. I am trying to match the GL code with the totals for that specific account. Columns A, B, and C show the state of my data prior to sorting it, and the lines under Intended Output show how I would like the data to output.
I am trying to automate this process, so I can paste data into columns A, B, & C in the raw format and have it automatically spit out the required numbers in the format of the Intended Output. The GL codes remain the same, but the numbers and the number of rows will change. I've color coded them for ease of review.
Thank you very much in advance!
Using a combination of the following formulas you can create a list of filtered results. It works on the principal that you Data1 text that you want to pull is the only text with a "-" in it, and that the totals you are pulling from Data2 and Data3 are the only numbers in the column. Any change to that pattern will most likely break the system. Note the formulas will not copy formatting.
IFERROR
INDEX
AGGREGATE
ROW
ISNUMBER
FIND
Lets assume the output will be place in a small table with E2 being the upper left data location.
In E2 use the following formula and copy down as needed:
=IFERROR(INDEX(A:A,AGGREGATE(15,6,ROW($A$1:$A$30)/ISNUMBER(FIND("-",$A$1:$A$30)),ROW(A1))),"")
In F2 use the following formula and copy to the right 1 column and down as needed:
=IFERROR(INDEX(B:B,AGGREGATE(15,6,ROW($A$1:$A$30)/ISNUMBER(B$1:B$30),ROW(A1))),"")
AGGREGATE performs array like calculations. As such, do not use full column references such as A:A in it as it can lead to excess calculations. Be sure to limit it to the range you are looking at.
Try this procedure:
Public Sub bruce_wayne()
'Assumptions
'1. Data spreadsheet will ALWAYS have the structure shown in the question
'2. The key word "Total" (or whatever else it might be) is otherwise NOT found
' anywhere else in the 1st data column
'3. output is written to the same sheet as the data
'4. As written, invoked when data sheet is the active sheet
'5. set the 1st 3 constants to the appropriate values
Const sData2ReadTopLeft = "A1" 'Top left cell of data to process
Const sData2WriteTopLeft = "J2" 'Top left cell of where to write output
Const sSearchText = "Total" 'Keyword for summary data
'*******************
Const sReplaceText = "Wakanda"
Dim r2Search As Range
Dim sAccountCode As String
Dim rSearchText As Range
Dim iRowsProcessed As Integer
Set r2Search = Range(sData2ReadTopLeft).EntireColumn
sAccountCode = Range(sData2ReadTopLeft).Offset(1, 0).Value
iRowsProcessed = 0
Do While Application.WorksheetFunction.CountIf(r2Search, sSearchText) > 0
Set rSearchText = r2Search.Find(sSearchText)
Range(sData2WriteTopLeft).Offset(iRowsProcessed, 0) = sAccountCode
Range(sData2WriteTopLeft).Offset(iRowsProcessed, 1) = rSearchText.Offset(0, 1).Value
Range(sData2WriteTopLeft).Offset(iRowsProcessed, 2) = rSearchText.Offset(0, 2).Value ' add this if there are more summary columns to return
'last two lines could be collapsed into a single line; at the expense of readability..
rSearchText.Value = sReplaceText 'so that next search will find the next instance of the trigger text
iRowsProcessed = iRowsProcessed + 1
sAccountCode = rSearchText.Offset(1, 0).Value
Loop
r2Search.Replace what:=sReplaceText, Replacement:=sSearchText
End Sub
I am working with listobjects in Excel and I have the following problem:
I add data to a table everytime a code is run.
Previously I have to delete all the old data.
ThisWorkbook.Sheets("comm").ListObjects(1).DataBodyRange.Delete
What happend afterwards is that I get an error with:
myNrofRowsinCOMM = COMMtbl.DataBodyRange.Rows.Count
I had a look to this post at no avail. I still dont understand what is going on.
I tried the follwoing as well:
MsgBox "COMMtbl.Range.Rows.Count:" & COMMtbl.Range.Rows.Count
MsgBox "COMMtbl.ListRows.Count:" & COMMtbl.ListRows.Count
MsgBox "COMMtbl.databodyRange.Rows.Count:" & COMMtbl.DataBodyRange.Rows.Count
If COMMtbl.Range.Rows.Count = 1 Then
COMMtbl.ListRows.Add (1)
End If
If the table is empty (row headers and an empty first row) the first line gives 2. So the range has 2 rows which seems according to reality.
COMMtbl.Range.Rows.Count=2
the sencond one gives 0. Which I dont understand at all.
COMMtbl.ListRows.Count=0
And the third one gives an error
"Object variable or withblcok variable not set"
I am trying to add rows to the table and fill them with data, for that I add a row and populate it. I want to add a row at the end, therefore I need everytime to count the number of rows. ALL fine except for the firts one when I previously deleted the whole content of the table which looks like:
Any help is welcome
Thanks a lot.
I needed a refresher, so this might help you too:
.
.Range - Includes the Header, Insert Row and Totals Row (if visible)
.DataBodyRange
- Contains the data area, between the Header Row and the Insert Row
- If the ListObject doesn't have a DataBodyRange, this property returns Null
.ListRows
- Represents all the rows of data (doesn't include Header, Total, or Insert rows)
- To delete any items from this collection, do not use the Delete method of the item
Use the Delete method of the range of the item to delete the item
For example ListRows.Item(1).Range.Delete()
.
When you do DataBodyRange.Delete the table doesn’t have the DataBodyRange object anymore, so to confirm that there are no rows with data in the table, replace
myNrofRowsinCOMM = COMMtbl.DataBodyRange.Rows.Count
with
myNrofRowsinCOMM = COMMtbl.ListRows.Count
More details from MSDN - ListObject
If you don't have data in the ListObject.DataBodyRange Is Nothing, so you can't count rows. You can get the last row of the ListObject by using n = ListObject.Range.Rows.Count and then ListObject.ListRows(n).Range
I don't know how the data you have at hand looks like, but for the sake of the example, if all you had was one column and one row, you could add the data to the last row without worrying if the table is empty or not and then using the .Add method in your example.
Dim current_n_rows As Integer
With ThisWorkbook.Sheets("comm").ListObjects(1)
.DataBodyRange.Delete
current_n_rows = .ListRows.Count
.ListRows(current_n_rows).Range.Value = NewData
.ListRows.Add
End With
You can wrap this statement around the loops you would need to fill the table.
Hope it helps! Cheers!
Has anyone come across a situation where Excel seems to manipulate your formulas.
I have a sheet where I have an Index value in Column A. The First row starts with any non zero Value. Subsequent rows in the column increment the value. Eg
A1 = 1000
A2= A1+ 1
A3= A2 + 1
and so on/
I have another column B whose values will either be blank or a formula pointing to column A(usually the subsequent rows)
Eg:
B1.Formula = "=A2"
B2.Formula = "=A3"
B3.Value = ""
B4.value = "=A6"
Now I have a backup-restore functionality that lets me write out the data/formulas to a text file and then read it back in another workbook.
In the case of columns A and B, I am checking if the text value starts with "=" and then set either the value or formula of that cell depending on whether there is a formula or not.
So far the functionality has worked fine. It lets me restore accurately.
Now, if I convert this data range to a table and modify the code accordingly the behaviour is strange. I am using the ListObject structure to refer to the table. So for Column B my restore code is:
If Left(soureString) = "=" Then
'This is a formula
Sheets("MySheet").ListObjects(1).ListColumns("Next").DataBodyRange(row).Formula = sourcestring
Else
'This is a value
Sheets("MySheet").ListObjects(1).ListColumns("Next").DataBodyRange(row).Value = soureString
End If
once I am done writing a row, I loop to the start and
Dim newRow AS listrow
Set newRow = Sheets("MySheet").Listrows.Add(AlwaysInsert:=False)
row = newRow.Index
But this time when I run the process. this is what I get:
B1.Formula = "=A5"
B2.Formula = "=A5"
B3.Value = ""
B4.value = "=A5"
Why are my formula values all changing to the same value when I use a table instead of a range?
I had the same issue when populating a ListObject (Table) from an Excel Add-in, setting AutoFillFormulasInLists was the solution.
My workaround is to save the current setting, set AutoFillFormulasInLists to false, populate the table with data, formulas etc, then set AutoFillFormulasInLists back to the original setting.
bool OriginalAutoFillFormulaInListsFlag = app.AutoCorrect.AutoFillFormulasInLists;
app.AutoCorrect.AutoFillFormulasInLists = false;
//[ListObject population code....]
if (OriginalAutoFillFormulaInListsFlag == true)
{
app.AutoCorrect.AutoFillFormulasInLists = true;
}
Hope this helps someone.
I faced a similar issue. Ideally you could tell excel to stop doing this but I haven't been able to figure out how. Supposedly doing the following is supposed to keep excel from copying the formulas:
xlApp.AutoCorrect.AutoFillFormulasInLists = false
but it didn't work for me.
Using the answer from this question How to create running total using Excel table structured references? helped me. It doesn't feel like the ideal solution but it does do the job.
I used this formula where Weight is a column name from my table. #This Row is a "Special item specifier" and has a special meaning. The syntax looks a little funky because it's what's called a Structured Reference:
=AVERAGE(INDEX([Weight],1):[[#This Row],[Weight]])
The INDEX([Weight],1) part gives the reference for the 1st row in the Weight column
While the [[#This Row],[Weight]] part gives the reference for the current row in the Weight column.
So for example, if Weight is column J, and the current row is, say, 7 then this is equivalent to
=AVERAGE(J1:J7)
and on the 8th row it will be equivalent to
=AVERAGE(J1:J8) and so on
I have found that the only way to solve the problem of formulas changing in Excel Tables when you insert in VBA is to insert at the first row of the table, NOT the bottom or the middle. You can sort after.
And I always select or reference the EntireRow to do my insert in the Worksheet object not in the table itself. I always put a table in its own Worksheet anyway using xx.Entirerow.Insert.