Option Strict On Imports System Imports System.Diagnostics Imports System.Collections.Generic Imports Inventor Public Class ContentCenterReplace Inherits ComponentReplacer Private m_ccFinder As ContentCenterFinder Public Sub New(ByVal doc As Document, ByVal app As Application, Optional ByVal Language As String = "en-US") MyBase.New(doc, app, "ContentCenterReplace") m_ccFinder = New ContentCenterFinder(app, Language) m_ccFinder.CustomPartDirectory = CustomPartDirectory End Sub Public Property CreationMode() As FileCreationMode Get Return m_ccFinder.CreationMode End Get Set(ByVal value As FileCreationMode) m_ccFinder.CreationMode = value End Set End Property Public Property KeyColumnName() As String Get Return m_ccFinder.KeyColumnName End Get Set(ByVal value As String) m_ccFinder.KeyColumnName = value End Set End Property Public Sub Replace(ByVal componentName As String, ByVal ccPath As String, ByVal partNumber As String) Dim occurrence As ComponentOccurrence = FindComponent(componentName) Replace(occurrence, ccPath, partNumber) End Sub Public Sub Replace(ByVal occurrence As ComponentOccurrence, ByVal ccPath As String, ByVal partNumber As String) Dim newFileName As String = GetPartFullFileName(ccPath, partNumber) ReplaceComponent(occurrence, newFileName) End Sub Public Sub ReplaceCustom(ByVal componentName As String, ByVal ccPath As String, ByVal partNumber As String, ByVal fileName As String, ByVal ParamArray customNamesAndValues() As Object) Dim occurrence As ComponentOccurrence = FindComponent(componentName) ReplaceCustom(occurrence, ccPath, partNumber, fileName, customNamesAndValues) End Sub Public Sub ReplaceCustom(ByVal occurrence As ComponentOccurrence, ByVal ccPath As String, ByVal partNumber As String, ByVal fileName As String, ByVal ParamArray customNamesAndValues() As Object) Dim newFileName As String = GetCustomPartFullFileName(ccPath, partNumber, fileName, customNamesAndValues) ReplaceComponent(occurrence, newFileName) End Sub Public Function GetPartFullFileName(ByVal ccPath As String, ByVal partNumber As String) As String m_ccFinder.CustomPartDirectory = CustomPartDirectory Dim newFileName As String = m_ccFinder.FindFileName(ccPath, partNumber, Nothing) CheckFileName(newFileName, ccPath, partNumber) Return newFileName End Function Public Function GetCustomPartFullFileName(ByVal ccPath As String, ByVal partNumber As String, ByVal fileName As String, ByVal ParamArray customNamesAndValues() As Object) As String m_ccFinder.CustomPartDirectory = CustomPartDirectory Dim customNv As NameValueMap = CreateCustomMap(customNamesAndValues) Dim customInfo As New CustomInformation() customInfo.CustomFileName = fileName customInfo.CustomNamesAndValues = customNv Dim newFileName As String = m_ccFinder.FindFileName(ccPath, partNumber, customInfo) CheckFileName(newFileName, ccPath, partNumber) Return newFileName End Function Private Sub CheckFileName(ByVal newFileName As String, ByVal ccPath As String, ByVal partNumber As String) If String.IsNullOrEmpty(newFileName) Then Throw New ArgumentException(String.Format("Content Center part not found: {0}:{1}", ccPath, partNumber)) End If End Sub Public Function GetValuesInColumn(ByVal familyPathName As String, ByVal columnName As String) As ArrayList Return m_ccFinder.GetValuesInColumn(familyPathName, columnName) End Function Public Function GetFamilyNames(ByVal pathName As String) As ArrayList Return m_ccFinder.GetFamilyNames(pathName) End Function Public Function GetMatchingFamilyNames(ByVal pathName As String, ByVal partialName As String, Optional ByVal options As FamilyMatchingOptions = FamilyMatchingOptions.Recurse) As Dictionary(Of String, String) Return m_ccFinder.GetMatchingFamilyNames(pathName, partialName, options) End Function Private Sub ReplaceComponent(ByVal occurrence As ComponentOccurrence, ByVal newFileName As String) ReplaceIfDifferent(occurrence, newFileName) End Sub Private Function CreateCustomMap(ByVal ParamArray customNamesAndValues() As Object) As NameValueMap Dim customNv As NameValueMap = m_app.TransientObjects.CreateNameValueMap() Dim currentName As String = String.Empty Dim index As Integer For Each obj As Object In customNamesAndValues If (index Mod 2 = 0) Then currentName = obj.ToString() Else customNv.Add(currentName, obj) End If index += 1 Next Return customNv End Function End Class Public Enum FileCreationMode ByFunctionName = 0 AsCustom AsStandard End Enum Public Class NonContentReplace Inherits ComponentReplacer Private m_sourcePartDirectory As String Public Sub New(ByVal doc As Document, ByVal app As Application) MyBase.New(doc, app, "NonContentReplace") m_sourcePartDirectory = IO.Path.GetDirectoryName(doc.FullFileName) End Sub Public Property SourcePartDirectory() As String Get Return m_sourcePartDirectory End Get Set(ByVal value As String) m_sourcePartDirectory = value End Set End Property Public Sub ReplaceCustom(ByVal componentName As String, ByVal templateFileName As String, ByVal generatedFileName As String, ByVal ParamArray parameterNamesAndValues() As Object) Dim occurrence As ComponentOccurrence = FindComponent(componentName) ReplaceCustom(occurrence, templateFileName, generatedFileName, parameterNamesAndValues) End Sub Public Sub ReplaceCustom(ByVal occurrence As ComponentOccurrence, ByVal templateFileName As String, ByVal generatedFileName As String, ByVal ParamArray parameterNamesAndValues() As Object) If (Not generatedFileName.EndsWith(".ipt", StringComparison.OrdinalIgnoreCase)) Then generatedFileName += ".ipt" End If Dim fullPath As String = IO.Path.Combine(CustomPartDirectory, generatedFileName) If (Not IO.File.Exists(fullPath)) Then GeneratePart(fullPath, templateFileName, parameterNamesAndValues) End If ReplaceIfDifferent(occurrence, fullPath) End Sub Sub GeneratePart(ByVal generatedFullFileName As String, ByVal templateFileName As String, ByVal parameterNamesAndValues() As Object) Dim templateFullFileName As String = IO.Path.Combine(m_sourcePartDirectory, templateFileName) Dim templatePart As Document = Nothing Try templatePart = m_app.Documents.Open(templateFullFileName, False) Catch ex As Exception Throw New ArgumentException("NonContentReplace: Cannot find (or open) the part: " + templateFullFileName, ex) End Try Try templatePart.SaveAs(generatedFullFileName, True) Catch ex As Exception Throw New ArgumentException("NonContentReplace: Cannot save the generated part: " + generatedFullFileName, ex) Finally templatePart.ReleaseReference() End Try If (parameterNamesAndValues.Length = 0) Then Return Dim errorHit As Boolean = False Dim parameterNotFound As String = String.Empty Dim generatedPart As Document = m_app.Documents.Open(generatedFullFileName, False) Try Dim partDoc As PartDocument = DirectCast(generatedPart, PartDocument) Dim partParams As Parameters = partDoc.ComponentDefinition.Parameters Dim currentName As String = String.Empty Dim index As Integer For Each obj As Object In parameterNamesAndValues If (index Mod 2 = 0) Then currentName = obj.ToString() Else Dim foundParameter As Boolean = AssignParameter(currentName, obj, partParams) If (Not foundParameter) Then parameterNotFound = currentName Exit For End If End If index += 1 Next generatedPart.Update() generatedPart.Save() Catch ex As Exception errorHit = True Finally generatedPart.ReleaseReference() End Try If (errorHit) Then Throw New ArgumentException("NonContentReplace: Error assigning parameters in the part: " + generatedFullFileName) End If If (Not String.IsNullOrEmpty(parameterNotFound)) Then Throw New ArgumentException(String.Format("NonContentReplace: No parameter named {0} was not found in the part {1}", parameterNotFound, generatedFullFileName)) End If End Sub Function AssignParameter(ByVal currentName As String, ByVal obj As Object, ByVal partParams As Parameters) As Boolean For Each param As Inventor.Parameter In partParams If (param.Name = currentName) Then If (param.Units = "Text") Then param.Value = obj.ToString() ElseIf (param.Units = "Boolean") Then param.Value = CBool(obj) Else param.Expression = obj.ToString() End If Return True End If Next Return False End Function End Class Public MustInherit Class ComponentReplacer Protected m_doc As Document Protected m_app As Inventor.Application Protected m_assemDoc As AssemblyDocument Protected m_customPartDirectory As String Protected m_replaceAll As Boolean Protected m_deleteOldFiles As Boolean Private m_derivedClassName As String Public Sub New(ByVal doc As Document, ByVal app As Application, ByVal derivedClassName As String) m_doc = doc m_app = app m_derivedClassName = derivedClassName m_assemDoc = TryCast(m_doc, AssemblyDocument) If (m_assemDoc Is Nothing) Then Throw New ArgumentException(m_derivedClassName & " can only be used in an assembly.") End If If (Not String.IsNullOrEmpty(m_doc.FullFileName)) Then CustomPartDirectory = IO.Path.GetDirectoryName(m_doc.FullFileName) End If End Sub Public Property CustomPartDirectory() As String Get Return m_customPartDirectory End Get Set(ByVal value As String) m_customPartDirectory = value End Set End Property Public Property ReplaceAll() As Boolean Get Return m_replaceAll End Get Set(ByVal value As Boolean) m_replaceAll = value End Set End Property Public Property DeleteOldFiles() As Boolean Get Return m_deleteOldFiles End Get Set(ByVal value As Boolean) m_deleteOldFiles = value End Set End Property Friend Sub ReplaceIfDifferent(ByVal occurrence As ComponentOccurrence, ByVal newFileName As String) Dim oldFileName As String = GetFileName(occurrence) If (String.Equals(oldFileName, newFileName, StringComparison.OrdinalIgnoreCase)) Then Trace.WriteLine(" Component doesn't need replacement: " & newFileName) Return End If occurrence.Replace(newFileName, ReplaceAll) If (DeleteOldFiles) Then DeleteFile(oldFileName) End Sub Friend Function GetFileName(ByVal occurrence As ComponentOccurrence) As String Dim oldFileName As String = String.Empty Try oldFileName = occurrence.ReferencedDocumentDescriptor.FullDocumentName Catch End Try Return oldFileName End Function Friend Function FindComponent(ByVal componentName As String) As ComponentOccurrence Dim compDef As AssemblyComponentDefinition = m_assemDoc.ComponentDefinition For Each compOcc As Inventor.ComponentOccurrence In compDef.Occurrences If String.Equals(compOcc.Name, componentName, StringComparison.OrdinalIgnoreCase) Then Return compOcc End If Next Throw New ArgumentException(m_derivedClassName + ": component not found: " & componentName) End Function Public Sub DeleteFile(ByVal fileName As String) If (String.IsNullOrEmpty(fileName)) Then Return If (Not fileName.StartsWith(CustomPartDirectory, StringComparison.OrdinalIgnoreCase)) Then Return ' Don't delete standard parts. End If m_app.Documents.CloseAll(UnreferencedOnly:=True) For Each doc As Document In m_app.Documents If (String.Equals(doc.FullFileName, fileName) AndAlso doc.Open) Then Return If (DocumentHasReferenceTo(doc, fileName)) Then Return Next Trace.WriteLine("Deleting file: " & fileName) Try IO.File.Delete(fileName) Catch ' We don't care if we can't delete. There is probably a good reason. End Try End Sub Function DocumentHasReferenceTo(ByVal doc As Document, ByVal fileName As String) As Boolean For Each docdesc As DocumentDescriptor In doc.ReferencedDocumentDescriptors If (docdesc.FullDocumentName.Contains("<")) Then Continue For ' Compare only filename, not path. The path may not be resolved yet. If (String.Equals(IO.Path.GetFileName(docdesc.FullDocumentName), IO.Path.GetFileName(fileName), StringComparison.OrdinalIgnoreCase)) Then Return True End If Next Return False End Function End Class Class ContentCenterFinder Private m_app As Application Private m_pathName As String Private m_pathLevels As String() ' n-1 levels are category names. Last level is family name Private m_partNumber As String Private m_ccDir As String Private m_customPartDirectory As String Private m_customInfo As CustomInformation Private m_keyColumnName As String Private m_creationMode As FileCreationMode Public Sub New(ByVal app As Application, Optional ByVal Language As String = "en-US") m_app = app m_ccDir = System.IO.Path.Combine(m_app.DesignProjectManager.ActiveDesignProject.ContentCenterPath, Language) m_creationMode = FileCreationMode.ByFunctionName End Sub Public Property CustomPartDirectory() As String Get Return m_customPartDirectory End Get Set(ByVal value As String) m_customPartDirectory = value End Set End Property Public Property KeyColumnName() As String Get Return m_keyColumnName End Get Set(ByVal value As String) m_keyColumnName = value End Set End Property Public Property CreationMode() As FileCreationMode Get Return m_creationMode End Get Set(ByVal value As FileCreationMode) m_creationMode = value End Set End Property Friend Function FindFileName(ByVal familyPathName As String, ByVal partNumber As String, ByVal customInfo As CustomInformation) As String CompensateForComDefect() m_partNumber = partNumber m_customInfo = customInfo Dim family As ContentFamily = FindFamily(familyPathName) Return FindFileByColumn(family) End Function Friend Function GetValuesInColumn(ByVal familyPathName As String, ByVal columnName As String) As ArrayList CompensateForComDefect() Dim family As ContentFamily = FindFamily(familyPathName) Dim values As New ArrayList Dim col As ContentTableColumn = FindColumn(family, columnName) If (col Is Nothing) Then Throw New ArgumentException("ContentCenterReplace.GetValuesInColumn: Column name not found: " & columnName) End If For Each row As ContentTableRow In family.TableRows Dim cell As ContentTableCell = row.Item(col.InternalName) values.Add(cell.Value) Next Return values End Function Friend Function GetFamilyNames(ByVal pathName As String) As ArrayList CompensateForComDefect() ParseLevels(pathName) Dim node As ContentTreeViewNode = FindNode(m_app.ContentCenter.TreeViewTopNode, 0) If (node Is Nothing) Then Throw New ArgumentException("GetFamilyNames: content center category not found: " & pathName) End If Dim names As New ArrayList For Each family As ContentFamily In node.Families names.Add(family.DisplayName) Next Return names End Function Friend Function GetMatchingFamilyNames(ByVal pathName As String, ByVal partialName As String, Optional ByVal options As FamilyMatchingOptions = FamilyMatchingOptions.Recurse) As Dictionary(Of String, String) CompensateForComDefect() ParseLevels(pathName) Dim node As ContentTreeViewNode = FindNode(m_app.ContentCenter.TreeViewTopNode, 0) If (node Is Nothing) Then Throw New ArgumentException("GetMatchingFamilyNames: content center category not found: " & pathName) End If Dim names As New Dictionary(Of String, String) AddMatchingFamilyNames(names, node, partialName, options, 0) Return names End Function Private Sub AddMatchingFamilyNames(ByVal names As Dictionary(Of String, String), ByVal node As ContentTreeViewNode, ByVal partialName As String, ByVal options As FamilyMatchingOptions, ByVal level As Integer) Dim categoryName As String = node.FullTreeViewPath For Each family As ContentFamily In node.Families If ((level > 0 AndAlso (options And FamilyMatchingOptions.MatchAllInSubCategories) <> 0) OrElse _ MatchesPartialName(family.DisplayName, partialName)) Then names(family.DisplayName) = categoryName + ":" + family.DisplayName End If Next If ((options And FamilyMatchingOptions.Recurse) = 0) Then Return For Each childNode As ContentTreeViewNode In node.ChildNodes If (MatchesPartialName(childNode.DisplayName, partialName)) Then AddMatchingFamilyNames(names, childNode, partialName, options, level + 1) End If Next 'Marshal.FinalReleaseComObject(node) End Sub Private Function MatchesPartialName(ByVal name As String, ByVal partialName As String) As Boolean Return name.IndexOf(partialName, StringComparison.OrdinalIgnoreCase) >= 0 End Function Private Function FindFamily(ByVal pathName As String) As ContentFamily ParseLevels(pathName) Dim fam As ContentFamily = FindFamily(m_app.ContentCenter.TreeViewTopNode, 0) If (fam Is Nothing) Then Throw New ArgumentException("Content Center family not found: " & pathName) End If Return fam End Function Private Sub ParseLevels(ByVal pathName As String) m_pathName = pathName If pathName.IndexOf(":"c) > 0 Then m_pathLevels = pathName.Split(":"c) Else Dim stringSeparators As String() = New String() {"->"} ' Support paths such as: "Fasteners->Pins->Cotter Pins" m_pathLevels = pathName.Split(stringSeparators, StringSplitOptions.None) End If End Sub Private Function FindFamily(ByVal parentNode As ContentTreeViewNode, ByVal level As Integer) As ContentFamily If level = m_pathLevels.Length - 1 Then If parentNode.Families.Count = 0 Then Return Nothing End If For Each fam As ContentFamily In parentNode.Families If fam.DisplayName = m_pathLevels(level) Then ' Debug.Print("--- ContentFamily Directory = " & fam.MemberDirectory) Return fam End If Next Return Nothing End If For Each cNode As ContentTreeViewNode In parentNode.ChildNodes If cNode.DisplayName = m_pathLevels(level) Then Return FindFamily(cNode, level + 1) End If Next Return Nothing End Function Private Function FindNode(ByVal parentNode As ContentTreeViewNode, ByVal level As Integer) As ContentTreeViewNode If level = m_pathLevels.Length Then Return parentNode End If For Each cNode As ContentTreeViewNode In parentNode.ChildNodes ' Trace.WriteLine(" --- Content node name: " & cNode.DisplayName) If cNode.DisplayName = m_pathLevels(level) Then Return FindNode(cNode, level + 1) End If Next Return Nothing End Function Private Function FindColumn(ByVal family As ContentFamily, ByVal columnName As String) As ContentTableColumn For i As Integer = 0 To 1 For Each col As ContentTableColumn In family.TableColumns ' Search first for InternalName, then for DisplayHeading. ' If the user knows the internal name, this is safe. If he only knows the display name, this should be OK ' but there could be localization problems. ' The InternalName is displayed as a tooltip when you mouse over the column title. So the user can find it. If ((i = 0 AndAlso String.Equals(col.InternalName, columnName, StringComparison.OrdinalIgnoreCase)) OrElse _ (i = 1 AndAlso String.Equals(col.DisplayHeading, columnName, StringComparison.OrdinalIgnoreCase))) Then Return col End If Next Next Return Nothing End Function Private Function FindFileByColumn(ByVal family As ContentFamily) As String ' Return the complete filename for this part number or filename ' Ask the Content Center to create the member if required 'Debug.Print(" Family: {0}", family.DisplayName); Dim keyColumnInternalName As String = String.Empty If (Not String.IsNullOrEmpty(KeyColumnName)) Then Dim keyColumn As ContentTableColumn = FindColumn(family, KeyColumnName) If (keyColumn Is Nothing) Then Throw New ArgumentException(String.Format("ContentCenterReplace: The column name {0} was not found in the famuly: {1}", KeyColumnName, family.DisplayName)) End If keyColumnInternalName = keyColumn.InternalName End If Dim partNumberColIndex As Integer = -1 Dim fileNameColIndex As Integer = -1 Dim keyColumnIndex As Integer = -1 Dim i As Integer = 1 For Each col As ContentTableColumn In family.TableColumns '' Debug.Print("{0}, Column: {1} ({2})", tabString, col.DisplayHeading, col.InternalName); If col.InternalName = "PARTNUMBER" Then partNumberColIndex = i ElseIf col.InternalName = "FILENAME" Then fileNameColIndex = i ElseIf col.InternalName = keyColumnInternalName Then keyColumnIndex = i End If i += 1 Next If partNumberColIndex < 0 OrElse fileNameColIndex < 0 Then Return String.Empty End If For Each row As ContentTableRow In family.TableRows Dim found As Boolean = False For Each cell As ContentTableCell In row If (cell.Column = keyColumnIndex OrElse _ keyColumnIndex = -1 And (cell.Column = partNumberColIndex OrElse cell.Column = fileNameColIndex)) Then If FilenamesAreEqual(cell.Value, m_partNumber) Then found = True Exit For End If End If Next If Not found Then Continue For ' Next Row End If Dim fileNameCell As ContentTableCell = row.Item(fileNameColIndex) Return GetOrCreateFilename(family, row, fileNameCell.Value) Next Return String.Empty End Function ''' ''' Find the file on disk, or create it from the Content Center. ''' ''' ''' ''' ''' filename with full path. Private Function GetOrCreateFilename(ByVal family As ContentFamily, ByVal row As ContentTableRow, ByVal fileNameCellValue As String) As String Dim asCustom As Boolean = m_customInfo IsNot Nothing If (m_creationMode = FileCreationMode.AsCustom) Then asCustom = True ElseIf (m_creationMode = FileCreationMode.AsStandard) Then asCustom = False End If Dim fileName As String = String.Empty Dim fileNameInput As String = String.Empty Dim fileNameLastLevel As String = String.Empty Dim customInput As NameValueMap = Nothing If (m_customInfo Is Nothing) Then fileNameLastLevel = MakeValidFileName(fileNameCellValue + ".ipt") Else fileNameLastLevel = MakeValidFileName(m_customInfo.CustomFileName + ".ipt") End If If (Not asCustom) Then Dim standardPath As String = IO.Path.Combine(m_ccDir, family.MemberDirectory) fileName = IO.Path.Combine(standardPath, fileNameLastLevel) Else fileName = IO.Path.Combine(CustomPartDirectory, fileNameLastLevel) End If If (m_customInfo IsNot Nothing) Then customInput = m_customInfo.CustomNamesAndValues fileNameInput = fileName End If If Not System.IO.File.Exists(fileName) Then Dim failureReason As Inventor.MemberManagerErrorsEnum Dim failureMessage As String = String.Empty Dim refresh As Inventor.ContentMemberRefreshEnum = ContentMemberRefreshEnum.kUseDefaultRefreshSetting Trace.WriteLine("-- ContentCenterFinder: Creating Part: " + fileName) ' string CreateMember(object Row, out Inventor.MemberManagerErrorsEnum FailureReason, ' out string FailureMessage, Inventor.ContentMemberRefreshEnum Refresh, ' bool Custom, string FileName, object CustomInput, object Options) fileName = family.CreateMember(row, failureReason, failureMessage, refresh, asCustom, fileNameInput, _ customInput, Nothing) End If Return fileName End Function Public Shared Function MakeValidFileName(ByVal fileName As String) As String Dim invalidChars As Char() = New Char() {"/"c, "\"c, ":"c, "<"c, ">"c, """"c} If (fileName.IndexOfAny(invalidChars) < 0) Then Return fileName End If For Each ch As Char In invalidChars fileName = fileName.Replace(ch, "_"c) Next Return fileName End Function Public Shared Function FilenamesAreEqual(ByVal nameA As String, ByVal nameB As String) As Boolean If (String.Equals(nameA, nameB, StringComparison.OrdinalIgnoreCase)) Then Return True If (String.Equals(MakeValidFileName(nameA), MakeValidFileName(nameB), StringComparison.OrdinalIgnoreCase)) Then Return True Return False End Function Private Sub CompensateForComDefect() ' The Content Center API will remember a previous child node, and it will not enumerate other siblings of the same parent. ' They are counting on the caller to release the reference. System.GC.Collect() End Sub End Class Class CustomInformation Public CustomNamesAndValues As NameValueMap Public CustomFileName As String End Class Public Enum FamilyMatchingOptions None = 0 Recurse = 1 MatchAllInSubCategories = 2 End Enum