How can I make removal of Strikethrough text in Excel cells using XML parsing more robust? - excel

I have a complex spreadsheet with many cells of text containing random mixtures of normal text and text with strikethrough. Before I scan a cell for useful information, I have to remove the struck through text. I intially achieved this (with VBA) using the Characters object, but it was so slow as to be totally impractical, for business purposes. I was then kindly supplied with some code (on this site) that parses the XML encoding. This was 1000's of times faster, but it occassionally causes the following error:
"The parameter node is not a child of this node".
So far, it only happens in heavily loaded cells (1000's of characters), otherwise it works fine. I cannot see anything wrong in the code or the XML structure of the problem cells, although I am a total newbie to XML. Using the VBA debugger, I know the error is occurring when RemoveChild() is called, typically when it has already worked without error on a few struck through sections of a cell's text.
Is there a way I could make the following code more robust?
Public Sub ParseCellForItems(TargetCell As Excel.Range, ItemsInCell() As String)
Dim XMLDocObj As MSXML2.DOMDocument60
Dim x As MSXML2.IXMLDOMNode
Dim s As MSXML2.IXMLDOMNode
Dim CleanedCellText As String
On Error GoTo ErrorHandler
Call UnstrikeLineBreakCharsInCell(TargetCell)
Set XMLDocObj = New MSXML2.DOMDocument60
'Add some namespaces.
XMLDocObj.SetProperty "SelectionNamespaces", "xmlns:ss='urn:schemas-microsoft-com:office:spreadsheet' " & _
"xmlns:ht='http://www.w3.org/TR/REC-html40'"
'Load the cell data as XML into XMLDOcObj.
If XMLDocObj.LoadXML(TargetCell.Value(xlRangeValueXMLSpreadsheet)) Then
Set x = XMLDocObj.SelectSingleNode("//ss:Data") 'Cell content.
If Not x Is Nothing Then
Set s = x.SelectSingleNode("//ht:S") 'Struck through cell content.
Do While Not s Is Nothing
x.RemoveChild s
Set s = x.SelectSingleNode("//ht:S")
Loop
CleanedCellText = XMLDocObj.Text
'Parse CleanedCellText for useful information.'
'...
End If
End If
Set XMLDocObj = Nothing
'Presumably don't have to 'destroy' x and s as well, as they were pointing to elements of XMLObj.
Exit Sub
ErrorHandler:
Call RaiseError(Err.Number, Err.Source, "ParseCellForItems()", Err.Description, Erl)
End Sub
Public Sub UnstrikeLineBreakCharsInCell(TargetCell As Excel.Range)
Dim mc As MatchCollection
Dim RegExObj1 As RegExp
Dim Match As Variant
On Error GoTo ErrorHandler
Set RegExObj1 = New RegExp
RegExObj1.Global = True
RegExObj1.IgnoreCase = True
RegExObj1.Pattern = "\n" 'New line. Equivalent to vbNewLine.
Set mc = RegExObj1.Execute(TargetCell.Value)
For Each Match In mc
TargetCell.Characters(Match.FirstIndex + 1, 1).Font.Strikethrough = False
Next Match
Set mc = Nothing
Set RegExObj1 = Nothing
Exit Sub
ErrorHandler:
Call RaiseError(Err.Number, Err.Source, "UnstrikeLineBreakCharsInCell()", Err.Description, Erl)
End Sub

Yep, as per Tim Williams' comment, making sure you're calling RemoveChild() from its immediate parent fixes the problem:
Set s = x.SelectSingleNode("//ht:S")
Do While Not s Is Nothing
s.ParentNode.RemoveChild s
Set s = x.SelectSingleNode("//ht:S")
Loop

Related

Constantly (but randomly) getting synchronization errors when accessing ChartData.Workbook

I'm writing a PowerPoint macro to update chart labels. Even though I found a correct algorithm and implemented it I still get two errors when accessing ChartData.Workbook. Code:
Public Sub EnterNewChLabsNumsCats( _
objChartData As ChartData, _
objCategoriesColl As CategoryCollection, _
dctExceptions As Dictionary)
Dim objCategory As ChartCategory
Dim i As Long
Dim strCategory As String
For i = 1 To objCategoriesColl.Count
Set objCategory = objCategoriesColl(i)
strCategory = objCategory.Name
'strAnyDigit - regex for detecting numbers inside a string
If strCategory Like strAnyDigit Then
Dim strArrCategory() As String
'strArrCleaned - function to clean non-printables from the string and divide it into array
strArrCategory() = strArrCleaned(strCategory, True, False)
If (Not Not strArrCategory()) <> 0 Then
Dim varI As Variant
For Each varI In strArrCategory()
'blnInDct - function to detect if a piece of the string is in a dictionary containing expressions not to change
If varI Like strAnyDigit And Not blnInDct(dctExceptions, CStr(varI)) Then
Dim strBefVarI As String
strBefVarI = CStr(varI)
Dim strAftVarI As String
'function to change the piece of the string - precisely a number
strAftVarI = ChangeFinalNumbersTextStrOnly(CStr(varI))
If strAftVarI <> strBefVarI Then
'this is where I get run-time error '-1328086627 (b0d7019d)': Method 'Activate' of object 'ChartData' failed
objChartData.Activate
'should I use Excel.Workbook? PowerPoint.ChartData.Workbook doesn't exist
Dim objChWkbk As Workbook
If objChWkbk Is Nothing Then Set objChWkbk = objChartData.Workbook
Dim objChWksh As Worksheet
Dim objChWkshRange As Range
With objChWkbk
'this is where I get run-time error '462': The remote server machine does not exist or is unavailable
.Application.WindowState = xlMinimized
.Application.ScreenUpdating = False
If objChWksh Is Nothing Then Set objChWksh = objChWkbk.Worksheets(1)
If objChWkshRange Is Nothing Then Set objChWkshRange = objChWksh.Cells
objChWkshRange.Replace strBefVarI, strAftVarI, xlPart, MatchCase:=False
.Close savechanges:=True
End With
End If
End If
Next varI
End If
End If
Next i
'opening ChartData.Workbook again because sometimes changes are not visible on a slide
Call UpdateCharts(objChartData)
If Not objChWkbk Is Nothing Then objChWkbk.Close savechanges:=True
Set objCategory = Nothing
Set objChWkshRange = Nothing
Set objChWksh = Nothing
Set objChWkbk = Nothing
End Sub
Interestingly, I get this errors totally randomly - there's no rule when it happens because often this code works smoothly. What possibly causes these problems and how to avoid them?* Is it an effect of a turned on 'Microsoft Excel 16.0 Object Library' (which I need in this macro)?
*I managed to do this using booleans and Do-Loop While structure but I doubt it's the only way.

Creating a multilevel list in Outlook from Excel VBA

I am working with excel to process user input and then output an standardized email based on the input, and then take that formatted text and save it to a variable to later add it to the clipboard for ease of entry into a system we use for internal documentation.
I have a functioning approach using HTML for the email format, but that doesn't resolve my intent to have it copy the code to the clipboard or variable as the HTML tags are copied as well. I'm hoping to get the functionality of Word's bullet lists so I've been trying to adapt the MS Word code in a way that can be called on demand.
I currently have the default excel libraries, form library and object library for Word and Outlook added to the program.
My goal is to pass an array list built on excel tables through the Word list and have it format and write the text to Word editor in an outlook draft. There will be varying number of sections (No more than 6) needed to be written, typically no more than 10 items per section, usually less. So I intend to have other sub/functions call this to format each section as needed.
Attached is an example of output for this section, along with an example of where the data is coming from. Each section will have it's own sheet in Excel. The second level of the list for each section will come from a separate sheet.
I included a portion of the actual code showing the startup of a new outlook draft and entry of text. EmailBody() currently just handles any text outside of these sections, and calls a separate function for each section to parse the tables (currently as unformatted text, and only inputting line breaks).
Output Example
Data source example
Sub Email()
Dim eTo As String
eTo = Range("H4").Value
Dim myItem As Object
Dim myInspector As Outlook.Inspector
Dim wdDoc As Word.Document
Dim wdRange As Word.Range
Set myItem = Outlook.Application.CreateItem(olMailItem)
With myItem
.To = eTo
.Bcc = "email"
.Subject = CNum("pt 1") & " | " & CNum("pt 2")
'displays message prior to send to ensure no errors in email. Autosend is possible, but not recommended.
.Display
Set myInspector = .GetInspector
'Obtain the Word.Document for the Inspector
Set wdDoc = myInspector.WordEditor
If Not (wdDoc Is Nothing) Then
Set wdRange = wdDoc.Range(0, wdDoc.Characters.Count)
wdRange.InsertAfter (EmailBody(CNum("pt 1"), CNum("pt 2")))
End If
'[...]
end with
end sub
Multilevel list code I am struggling to adapt. I keep getting an error on the commented out section of code, and unsure of how to properly correct it so that it both functions and can be called on demand:
Run-time error '450': Wrong number of arguments or invalid property
assignment
Sub testList()
Dim arr1 As Object
Set arr1 = CreateObject("System.Collections.ArrayList")
With arr1
.Add "test" & " $100"
.Add "apple"
.Add "four"
End With
Dim i As Long
With ListGalleries(wdBulletGallery).ListTemplates(1).ListLevels(1)
.NumberFormat = ChrW(61623)
.TrailingCharacter = wdTrailingTab
.NumberStyle = wdListNumberStyleBullet
.NumberPosition = InchesToPoints(0.25)
.Alignment = wdListLevelAlignLeft
.TextPosition = InchesToPoints(0.5)
.TabPosition = wdUndefined
.ResetOnHigher = 0
.StartAt = 1
.LinkedStyle = ""
End With
ListGalleries(wdBulletGallery).ListTemplates(1).Name = ""
'Selection.Range.ListFormat.ApplyListTemplateWithLevel ListTemplate:= _
' ListGalleries(wdBulletGallery).ListTemplates(1), ContinuePreviousList:= _
' False, ApplyTo:=wdListApplyToWholeList, DefaultListBehavior:= _
' wdWord10ListBehavior
'writes each item in ArrayList to document
For i = 0 To arr1.Count - 1
Selection.TypeText Text:=arr1(i)
Selection.TypeParagraph
Next i
'writes each item to level 2 list
Selection.Range.SetListLevel Level:=2
For i = 0 To arr1.Count - 1
Selection.TypeText Text:=arr1(i)
Selection.TypeParagraph
Next i
Selection.Range.ListFormat.RemoveNumbers NumberType:=wdNumberParagraph
arr1.Clear
End Sub
Please forgive me if any of this seems inefficient, or an odd approach. I literally pickup up VBA a few weeks ago and only have a few hours of application in between my job responsibilities with what I've learned so far. Any assistance would be much appreciated.
The reason why you are getting that error is because, it is not able to resolve the object Selection. You need to fully qualify the Selection object else Excel will refer to the current selection from Excel.
You may have referenced the Word Object library from Excel but that is not enough. The simplest way to reproduce this error is by running this from Excel
Sub Sample()
Selection.Range.ListFormat.ApplyListTemplateWithLevel ListTemplate:= _
ListGalleries(wdBulletGallery).ListTemplates(1), ContinuePreviousList:= _
False, ApplyTo:=wdListApplyToWholeList, DefaultListBehavior:= _
wdWord10ListBehavior
End Sub
Here is a sample code which will work. To test this, open a word document and select some text and then run this code from Excel
Sub Sample()
Dim wrd As Object
Set wrd = GetObject(, "word.application")
wrd.Selection.Range.ListFormat.ApplyListTemplateWithLevel ListTemplate:= _
ListGalleries(wdBulletGallery).ListTemplates(1), ContinuePreviousList:= _
False, ApplyTo:=wdListApplyToWholeList, DefaultListBehavior:= wdWord10ListBehavior
End Sub
Applying this to your code. You need to work with the Word objects and fully qualify your objects like Word Application, Word Document, Word Range etc. For example
Dim oWordApp As Object, oWordDoc As Object
Dim FlName As String
FlName = "C:\MyFile.Docx"
'~~> Establish an Word application object
On Error Resume Next
Set oWordApp = GetObject(, "Word.Application")
If Err.Number <> 0 Then
Set oWordApp = CreateObject("Word.Application")
End If
Err.Clear
On Error GoTo 0
oWordApp.Visible = True
Set oWordDoc = oWordApp.Documents.Open(FlName)
With oWordDoc
'
'~~> Rest of the code here
'
End With
Utilizing Word lists, while functional in this circumstance created a certain tedium in coding due to the need to declare both Word and Outlook objects and resolve their relation to each other.
It appears I was declaring my HTML lists incorrectly in my original code. I shifted the margin of the <li> rather than nesting <ul> to step the list.
By nesting the HTML list tags you can get the same functionality of a word list and the formatting will persist when copied to other text editors. However, copying must be done after it is written to .HTMLBody.
<ul><li>Apple</li><ul><li>Fruit</li></ul></ul>
or for VBA:
.HTMLBody = "<ul><li>" & arg1 & "</li><ul><li>" & arg2 & "</li></ul></ul>"
The above will output this to .HTMLBody:
AppleFruit
To copy the text you only need to select all the text in the Outlook word editor and then assign it to the clipboard if pasting as is, or assign it to a variable if additional changes are needed before putting it in the clipboard.

How do I copy the contents of one word document to the end of another using vba?

Goal for my project:
I want to be able to copy the contents of one document and append that selection to the end of another document.
What it does... (this is just background info so you understand why I am trying to do this):
I am trying to dynamically produce a document which quotes a variety of information regarding different parts and materials involved for a product.
The document itself has a consistent format which I have broken down and separated into two documents. The first contains a bunch of data that needs to be entered manually, and is where I want to append all additional content. The second contains roughly a dozen custom fields which are updated from an excel spreadsheet in VBA. For a single part and as a single doc this works as I want it (my base case). However my issue is when there are multiple parts for a project.
The Problem:
For multiple parts I have to store information in an array which changes in size dynamically as each additional part is added. When someone has added all the necessary parts they can select a button called "Create Quote".
Create quote runs a procedure which creates/opens separate copies of the two template documents mentioned above (saved on my computer). It then iterates through the array of parts and updates all the custom field in the 2nd document (no problems). Now I just need the contents of the 2nd document appended to the end of the first which is my problem.
What I want:
Ideally, my procedure will continue to iterate through every part in the array - updating custom fields, copy then paste the updated text, repeat... Until every part is included in the newly generated quote.
What I Tried - this code can be found in my generate quote procedure
I have tried many of the examples and suggestions provided by people who had similar question, but I don't know if its because I am operating from an excel doc, but many of their solution have not worked for me.
This is my most recent attempt and occurs after each iteration of the for loop
wrdDoc2.Fields.Update 'Update all the fields in the format document
wrdDoc2.Activate
Selection.WholeStory ' I want to select the entire document
Selection.Copy ' Copy the doc
wrdDoc1.Activate ' Set focus to the target document
Selection.EndKey wdStory ' I want the selection to be pasted to the end of the document
Selection.PasteAndFormat wdPasteDefault
QUOTE PROCEDURE - I am only including a handful of the fields I am updating because its not necessary to show them all
Private Sub quote_button_Click()
On Error GoTo RunError
Dim wrdApp1, wrdApp2 As Word.Application
Dim wrdDoc1, wrdDoc2 As Word.Document
Set wrdApp1 = CreateObject("Word.Application")
Set wrdApp2 = CreateObject("Word.Application")
wrdApp1.Visible = True
wrdApp2.Visible = True
Set wrdDoc1 = wrdApp1.Documents.Add(Template:="C:\MWES\AQT_v1.1(start).docm", NewTemplate:=False, DocumentType:=0)
Set wrdDoc2 = wrdApp2.Documents.Add(Template:="C:\MWES\AQT_v2.1(format).docm", NewTemplate:=False, DocumentType:=0)
Dim propName As String
For i = LBound(part_array, 1) To UBound(part_array, 1)
For Each prop In wrdDoc2.CustomDocumentProperties
propName = prop.name
' Looks for and sets the property name to custom values of select properties
With wrdDoc2.CustomDocumentProperties(propName)
Select Case propName
Case "EST_Quantity"
.value = part_array(i, 0) ' "QTY" ' Sheet1.Cells(1, 3) 'NA
Case "EST_Metal_Number"
.value = part_array(i, 1) ' "METAL_#" ' Sheet1.Cells(2, 3) 'NA"
Case "EST_Metal_Name"
.value = part_array(i, 2) ' "METAL_N" ' Sheet1.Cells(5, 2)
End Select
End With
Next prop ' Iterates until all the custom properties are set
wrdDoc2.Fields.Update 'Update all the fields in the format document
wrdDoc2.Activate
Selection.WholeStory ' I want to select the entire document
Selection.Copy ' Copy the doc
wrdDoc1.Activate ' Set focus to the target document
Selection.EndKey wdStory ' I want the selection to be pasted to the end of the document
Selection.PasteAndFormat wdPasteDefault
Next i ' update the document for the next part
RunError: ' Reportd any errors that might occur in the system
If Err.Number = 0 Then
Debug.Print "IGNORE ERROR 0!"
Else
Dim strError As String
strError = "ERROR: " & Err.Number & vbCrLf & Err.Description & vbCrLf & Erl
MsgBox strError
Debug.Print strError & " LINE: " & Erl
End If
End Sub
I apologize this was so long winded. Let me know if there is anything confusing or you may want clarified. I think I included everything though.
I think you're close, so here are a couple of comments and an example.
First of all, you're opening two separate MS Word Application objects. You only need one. In fact, it's possible that the copy/paste is failing because you're trying to copy from one Word app to a document opened in the other. (Trust me, I've seen weird things like this.) My example below shows how to do this by only opening a single application instance.
Dim mswApp As Word.Application
Set mswApp = AttachToMSWordApplication() 'more on this function below...
Dim doc1 As Word.Document
Dim doc2 As Word.Document
Set doc1 = mswApp.Documents.Open("C:\Temp\combined.docx")
Set doc2 = mswApp.Documents.Open("C:\Temp\control.docx")
While I don't often write code for Word, I've found that there are so many different ways to get at the same content using different objects or properties. This is always a source of confusion.
Based on this answer, which has worked well for me in the past, I then set up the source and destination ranges to perform the "copy":
Dim destination As Word.Range
Dim source As Word.Range
Set source = doc1.Content
Set destination = doc2.Content
destination.Collapse Direction:=Word.wdCollapseEnd
destination.FormattedText = source
Here is the whole module for reference:
Option Explicit
Sub AddDocs()
Dim wordWasRunning As Boolean
wordWasRunning = IsMSWordRunning()
Dim mswApp As Word.Application
Set mswApp = AttachToMSWordApplication()
Dim doc1 As Word.Document
Dim doc2 As Word.Document
Set doc1 = mswApp.Documents.Open("C:\Temp\combined.docx")
Set doc2 = mswApp.Documents.Open("C:\Temp\control.docx")
Dim destination As Word.Range
Dim source As Word.Range
Set source = doc1.Content
Set destination = doc2.Content
destination.Collapse Direction:=Word.wdCollapseEnd
destination.FormattedText = source
doc2.Close SaveChanges:=True
doc1.Close
If Not wordWasRunning Then
mswApp.Quit
End If
End Sub
Here's the promised note on a couple functions I use in the sample. I've built up a set of library functions, several of which help me access other Office applications. I save these modules as .bas files (by using the Export function in the VBA Editor) and import them as needed. So if you'd like to use it, just save the code below in using a plain text editor (NOT in the VBA Editor!), then import that file into your project.
Suggested filename is Lib_MSWordSupport.bas:
Attribute VB_Name = "Lib_MSWordSupport"
Attribute VB_Description = "Variety of support functions operating on MS Word"
Option Explicit
Public Function IsMSWordRunning() As Boolean
Attribute IsMSWordRunning.VB_Description = "quick check to see if an instance of MS Word is running"
'--- quick check to see if an instance of MS Word is running
Dim msApp As Object
On Error Resume Next
Set msApp = GetObject(, "Word.Application")
If Err > 0 Then
'--- not running
IsMSWordRunning = False
Else
'--- running
IsMSWordRunning = True
End If
End Function
Public Function AttachToMSWordApplication() As Word.Application
Attribute AttachToMSWordApplication.VB_Description = "finds an existing and running instance of MS Word, or starts the application if one is not already running"
'--- finds an existing and running instance of MS Word, or starts
' the application if one is not already running
Dim msApp As Word.Application
On Error Resume Next
Set msApp = GetObject(, "Word.Application")
If Err > 0 Then
'--- we have to start one
' an exception will be raised if the application is not installed
Set msApp = CreateObject("Word.Application")
End If
Set AttachToMSWordApplication = msApp
End Function

Importing an excel file to existing DataGridView in VB.Net

I have a problem with my below code which freezes and does nothing while running.
INFRASTRUCTURE:
Visual Studio 2017
.NET Framework 4.7.2
OS: Windows 7
GOAL:
I have a DataGridView which has 3 columns, I'm listing some information in this DataGridView from another source to fill first and second row.
The first row is the parameter name and the second one is this parameter's current value.
The third column is not filling in this process, I will fill that 3rd column from my database Excel file to compare the parameter's current value with the database value stored in Excel. This is where my problems start.
I'm trying to use below code for filling DataGridView parameter value from Excel sheet which I'm using a database;
Some parameters not stored in an Excel sheet so in fact, I need a function like vlookup to map data with the parameter name.
In excel A column is my parameter name and B column is the parameter's database value.
I'm trying to import this excel and trying to match parameter names in DataGridView and Excel if the parameter name is same it should writing Excel parameter value to a 3rd column in DataGridView.
Public Class BuildImportExcel
Public Shared Sub NewMethod(ByVal dgv As DataGridView)
Dim ofd As OpenFileDialog = New OpenFileDialog With {
.Filter = "Excel |*.xlsx",
.Title = "Import Excel File"
}
ofd.ShowDialog()
Try
If ofd.FileName IsNot "" Then
Dim xlApp As New Excel.Application
If xlApp Is Nothing Then
MessageBox.Show("Excel is not properly installed!")
Else
Dim xlBook As Excel.Workbook = xlApp.Workbooks.Open(ofd.FileName)
Dim xlSheet As Excel.Worksheet = CType(xlBook.ActiveSheet, Excel.Worksheet)
For i = 0 To dgv.Rows.Count
If dgv.Rows(i).Cells(0).Value IsNot "" Then
Dim look As Boolean = True
Dim found As Boolean = False
Dim rowLook As Integer = 2
Dim rowFound As Integer = 0
While look = True
If xlSheet.Range("A" & rowLook).Value IsNot "" Then
If xlSheet.Range("A" & rowLook).Text Is dgv.Rows(i).Cells(0).Value Then
found = True
rowFound = rowLook
End If
Else
look = False
End If
rowLook = rowLook + 1
End While
If found = True Then
dgv.Rows(i).Cells(2).Value = xlSheet.Range("B" & rowFound).Text
End If
End If
Next
xlApp.Quit()
Release(xlSheet)
Release(xlBook)
Release(xlApp)
End If
End If
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
Private Shared Sub Release(ByVal sender As Object)
Try
If sender IsNot Nothing Then
Marshal.ReleaseComObject(sender)
sender = Nothing
End If
Catch ex As Exception
sender = Nothing
End Try
End Sub
End Class
But the problem is it's not working freezing I thought parameter list about 200 rows so it causes to freezing and try it with small portions like 5 parameters and still same. Seems something wrong and I couldn't find it.
Also is it logic way to match them with that method or do you suggest anything like OLEDB connection?
EDIT:
I turn off Option Strict and then change IsNot to operator <> and then it start to work but i'd like to use Option Strict On how can i handle this operators?
You are making this more difficult than it has to be. A questionable and problematic area is the While look = True loop. This assumes a lot and, in my test, will cause the code to freeze often. The main problem here is that the code is (somewhat) looping through the rows from the Excel file. The issue here is that you do not KNOW how may rows there are! There is no checking for this and the code will fail on the line…
If xlSheet.Range("A" & rowLook).Value IsNot "" Then
When you reach the bottom of the given Excel file and it never finds a match.
Another issue is the line…
If xlSheet.Range("A" & rowLook).Text Is dgv.Rows(i).Cells(0).Value Then
This is ALWAYS going to fail and will never be true. My understanding…
The Is operator determines if two object references refer to the same
object. However, it does not perform value comparisons.
Given this, I recommend you simplify things a bit. Most important are the loops through the Excel worksheet which is “expensive” and if there is a lot of rows, you may have a performance problem. When I say a lot of rows, I mean tens of thousands of rows…. 200 rows should be fine.
It would un-complicate things if we could tell how many rows from the worksheet are returned from….
Dim xlSheet As Excel.Worksheet = CType(xlBook.ActiveSheet, Excel.Worksheet)
This is basically an Excel Range, and it is from this Range that we can get the number of rows in this excel range with…
Dim totalExcelRows = xlSheet.UsedRange.Rows.Count
Now, instead of the complicated and problematic While loop, we can replace it with a simple for loop. This will eliminate some variables and will keep the loop index in row range in the excel file.
I hope this makes sense… below is an example of what is described above.
Public Shared Sub NewMethod(ByVal dgv As DataGridView)
Dim ofd As OpenFileDialog = New OpenFileDialog With {
.Filter = "Excel |*.xlsx",
.Title = "Import Excel File",
.InitialDirectory = "D:\\Test\\ExcelFiles"
}
ofd.ShowDialog()
Try
If ofd.FileName IsNot "" Then
Dim xlApp As New Excel.Application
If xlApp Is Nothing Then
MessageBox.Show("Excel is not properly installed!")
Else
Dim xlBook As Excel.Workbook = xlApp.Workbooks.Open(ofd.FileName)
Dim xlSheet As Excel.Worksheet = CType(xlBook.ActiveSheet, Excel.Worksheet)
Dim totalExcelRows = xlSheet.UsedRange.Rows.Count
For i = 0 To dgv.Rows.Count
If dgv.Rows(i).Cells(0).Value IsNot Nothing Then
For excelRow = 1 To totalExcelRows
If xlSheet.Range("A" & excelRow).Text.ToString() = dgv.Rows(i).Cells(0).Value.ToString() Then
dgv.Rows(i).Cells(2).Value = xlSheet.Range("B" & excelRow).Text
Exit For
End If
Next
Else
Exit For
End If
Next
xlApp.Quit()
Marshal.ReleaseComObject(xlSheet)
Marshal.ReleaseComObject(xlBook)
Marshal.ReleaseComObject(xlApp)
End If
End If
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub

Excel VBA Find Duplicates and post to different sheet

I keep having an issue with some code in VBA Excel was looking for some help!
I am trying to sort through a list of names with corresponding phone numbers, checking for multiple names under the same phone number. Then post those names to a separate sheet.
So far my code is:
Sub main()
Dim cName As New Collection
For Each celli In Columns(3).Cells
Sheets(2).Activate
On Error GoTo raa
If Not celli.Value = Empty Then
cName.Add Item:=celli.Row, Key:="" & celli.Value
End If
Next celli
On Error Resume Next
raa:
Sheets(3).Activate
Range("a1").Offset(celli.Row - 1, 0).Value = Range("a1").Offset(cName(celli.Value) - 1, 0).Value
Resume Next
End Sub
When I try to run the code it crashes Excel, and does not give any error codes.
Some things I've tried to fix the issue:
Shorted List of Items
Converted phone numbers to string using cstr()
Adjusted Range and offsets
I'm pretty new to all this, I only managed to get this far on the code with help from other posts on this site. Not sure where to go with this since it just crashes and gives me no error to look into. Any ideas are appreciated Thank you!
Updated:
Option Explicit
Dim output As Worksheet
Dim data As Worksheet
Dim hold As Object
Dim celli
Dim nextRow
Sub main()
Set output = Worksheets("phoneFlags")
Set data = Worksheets("filteredData")
Set hold = CreateObject("Scripting.Dictionary")
For Each celli In data.Columns(3).Cells
On Error GoTo raa
If Not IsEmpty(celli.Value) Then
hold.Add Item:=celli.Row, Key:="" & celli.Value
End If
Next celli
On Error Resume Next
raa:
nextRow = output.Range("A" & Rows.Count).End(xlUp).Row + 1
output.Range("A" & nextRow).Value = data.Range("A1").Offset(hold(celli.Value) - 1, 0).Value
'data.Range("B1").Offset(celli.Row - 1, 0).Value = Range("B1").Offset(hold
Resume Next
End Sub
Update2:
Used hold.Exists along with an ElseIf to remove the GoTo's. Also changed it to copy and paste the row to the next sheet.
Sub main()
Set output = Worksheets("phoneFlags")
Set data = Worksheets("filteredData")
Set hold = CreateObject("Scripting.Dictionary")
For Each celli In data.Columns(2).Cells
If Not hold.Exists(CStr(celli.Value)) Then
If Not IsEmpty(celli.Value) Then
hold.Add Item:=celli.Row, Key:="" & celli.Value
Else
End If
ElseIf hold.Exists(CStr(celli.Value)) Then
data.Rows(celli.Row).Copy (Sheets("phoneFlags").Cells(Rows.Count, 1).End(xlUp).Offset(1, 0))
'output.Range("A" & nextRow).Value = data.Range("A1").Offset(hold(celli.Value) - 1, 0).Value
End If
Next celli
End Sub
When developing code, don't try (or be afraid of) errors as they are pointers to help fix the code or the logic. As such, don't use On Error unless it is absolutely indicated in the coding algorithm (*). using On Error when not necessary only hides errors, does not fix them and when coding it is always better to avoid the errors in the first place (good logic).
When adding to the Dictionary, first check to see if the item already exists. The Microsoft documentation notes that trying to add an element that already exists causes an error. An advantage that the Dictionary object has over an ordinary Collection object in VBA is the .exists(value) method, which returns a Boolean.
The short answer to your question, now that I have the context out of the way, is that you can first check (if Not hold.exists(CStr(celli.Value)) Then) and then add if it does not already exist.
(*) As a side note, I was solving an Excel macro issue yesterday which took me most of the day to nut out, but the raising of errors and the use of debugging code helped me make some stable code rather than some buggy but kind-of-working code (which is what I was fixing in the first place). However, the use of error handling can be a short cut in some instances such as:
Function RangeExists(WS as Worksheet, NamedRange as String) As Boolean
Dim tResult as Boolean
Dim tRange as Range
tResult = False ' The default for declaring a Boolean is False, but I like to be explicit
On Error Goto SetResult ' the use of error means not using a loop through all the named ranges in the WS and can be quicker.
Set tRange = WS.Range(NamedRange) ' will error out if the named range does not exist
tResult = True
On Error Goto 0 ' Always good to explicitly limit where error hiding occurs, but not necessary in this example
SetResult:
RangeExists = tResult
End Function

Resources