NotesNoteCollection allows you to obtain a handle on any notes in a database (data, design elements, ACL, etc.), based on specified criteria. NotesDXLExporter and NotesDXLImporter allow you to, respectively, export one or more notes to DXL and create notes by importing DXL into a database. Finally, the NotesStream class acts as temporary storage for the exported information. Combining these three classes, you can take periodic or on-demand snapshots of any design element and roll back to previous versions. NotesDXLExporter can also pipeline directly to NotesDXLImporter, which allows you to easily copy design elements from one database to another. Create a database containing the following design elements. 1. A script library to store version snapshot and rollback procedures, so that they can be called from anywhere in the database (code examples listed in the "See the code for this tip" link below). 2. A design element record form to store information about the design elements under version control. The form should contain, at minimum, fields for the replica ID of the database containing the design element, the current note ID of the element, a name for the element (I recommend referencing the $Title item that all design elements contain) and the database's current server and file path. Other useful fields to track are the type of element (i.e., form, view, agent), names of current lock holders, the template from which the parent database inherits, the designer version most recently used to edit the element, etc. Add a button to this form that calls the CreateElementVersion procedure. 3. A version record form to store the converted data for each version of a given design element. This form should contain the same fields as the first form, as well as fields to store the version number, documenter, timestamp and comment. To protect the stored versions, I recommend including a single "Continue = False" statement in the QueryOpen of the form; once a version document is created, it need never be opened. All necessary manipulation of version documents (i.e., rollback) can be performed from a view. 4. An agent to populate design element records (code examples listed in the "See the code for this tip" link below). 5. A view displaying documents created with the design element record form, sorted by the concatenation of the database replica ID field and note ID field. This view is used by the above agent to establish uniqueness. 6. A view displaying documents created with the version record form with the same sorting. Embed this view on the design element record form, listing the above concatenation as the single category string, to display only the version history for the design element currently open. Include a button in this view to call the RollbackToVersion procedure. 7. Additional views as desired sorting design element records by the various attributes you wish to track. I recommend including a button in all design element record views that allows a snapshot of multiple elements to be taken simultaneously. If you frequently have multiple developers working on the same project, I'd also recommend using this same approach to create check in/check out buttons that lock and unlock the design elements, in addition to just taking snapshots of them. This can be done by using the value of the Notes ID field to get a handle on the design element as a NotesDocument, then calling the Lock or UnLock method. 8. You might also find it useful to add a checkbox field to the design element record form indicating whether periodic snapshots are enabled for a given element, and create an agent to grab, say, an hourly snapshot for each enabled element. Having the ability to roll back even when versions haven't been manually created can be a lifesaver, but will naturally eat up disk space in a hurry, so if you take this approach you may want to keep a distinction between on-demand snapshots and those periodically grabbed by the agent, and purge the latter at a regular interval. Db code components: %REM Agent to populate design element records: Declarations: %END REM Dim gnsesCurrent As NotesSession Dim gndbCurrent As NotesDatabase Dim gnvwElementProfiles As NotesView Sub Initialize On Error Goto processerror Dim ndirCurrent As NotesDbDirectory Dim ndbScan As NotesDatabase Set gnsesCurrent = New NotesSession Set gndbCurrent = gnsesCurrent.CurrentDatabase Set gnvwElementProfiles = gndbCurrent.GetView("DEByRepID") Set ndirCurrent = gnsesCurrent.GetDbDirectory (gndbCurrent.Server) Set ndbScan = ndirCurrent.GetFirstDatabase(TEMPLATE_CANDIDATE) Do While Not (ndbScan Is Nothing) Call ndbScan.Open("","") UpdateAllCategories ndbScan Set ndbScan = ndirCurrent.GetNextDatabase Loop Exit Sub processerror: Print {Error } & Str(Err) & { in line } & Str(Erl) & { of Refresh Catalog - Initialize: "} & Error$ & {"} Resume Next End Sub Sub UpdateAllCategories (pndbCatalogEntry As NotesDatabase) On Error Goto processerror UpdateCategory pndbCatalogEntry, "Form" UpdateCategory pndbCatalogEntry, "Agent" UpdateCategory pndbCatalogEntry, "Folder" UpdateCategory pndbCatalogEntry, "Frameset" UpdateCategory pndbCatalogEntry, "Image Resource" 'You get the idea... add a sub call for each element type you want to track Exit Sub processerror: Print {Error } & Str(Err) & { in line } & Str(Erl) & { of Refresh Catalog - UpdateAllCategories: "} & Error$ & {"} Resume Next End Sub Sub UpdateCategory (pndbCatalogEntry As NotesDatabase, Byval pstrElementType As String) On Error Goto processerror Dim nncElementType As NotesNoteCollection Dim ndocPlaceHolder As NotesDocument Dim ndocElementProfile As NotesDocument Dim nitmElementInfo As NotesItem Dim nnamServer As NotesName Dim intCollectionLoop As Integer Dim strServerAbbreviatedName Dim strNoteID As String Dim strProfileKeys(1) As String If pndbCatalogEntry.Server = "" Then Set nnamServer = gnsesCurrent.CreateName(gnsesCurrent.EffectiveUserName) Else Set nnamServer = gnsesCurrent.CreateName(pndbCatalogEntry.Server) End If strServerAbbreviatedName = nnamServer.Abbreviated Set nncElementType = pndbCatalogEntry.CreateNoteCollection(False) Select Case pstrElementType Case "Form" nncElementType.SelectForms = True Case "Agent" nncElementType.SelectAgents = True Case "Folder" nncElementType.SelectFolders = True Case "Frameset" nncElementType.SelectFrameSets = True Case "Image Resource" nncElementType.SelectImageResources = True 'Again, to track additional element types, you would need to add a corresponding case statement End Select Call nncElementType.BuildCollection strNoteID = nncElementType.GetFirstNoteId For intCollectionLoop = 1 To nncElementType.Count strProfileKeys(0) = pndbCatalogEntry.ReplicaID strProfileKeys(1) = strNoteID Set ndocPlaceHolder = pndbCatalogEntry.GetDocumentByID(strNoteID) Set ndocElementProfile = gnvwElementProfiles.GetDocumentByKey(strProfileKeys, True) If ndocElementProfile Is Nothing Then Set ndocElementProfile = gndbCurrent.CreateDocument With ndocElementProfile .Form = "DesignElement" .ElementServer = strServerAbbreviatedName .ElementDatabase = pndbCatalogEntry.FilePath .ElementNoteID = strNoteID .ElementType = pstrElementType .ElementLockHolders = ndocPlaceHolder.LockHolders .ElementLastModified = ndocPlaceHolder.LastModified .ElementReplicaID = strProfileKeys(0) End With Set nitmElementInfo = ndocPlaceHolder.GetFirstItem("$Class") If Not (nitmElementInfo Is Nothing) Then Call nitmElementInfo.CopyItemToDocument(ndocElementProfile, "ElementClass") Set nitmElementInfo = ndocPlaceHolder.GetFirstItem("$Title") If Not (nitmElementInfo Is Nothing) Then Call nitmElementInfo.CopyItemToDocument(ndocElementProfile, "ElementName") Set nitmElementInfo = ndocPlaceHolder.GetFirstItem("$DesignerVersion") If Not (nitmElementInfo Is Nothing) Then Call nitmElementInfo.CopyItemToDocument(ndocElementProfile, "ElementDesignerVersion") Call ndocElementProfile.Save(True, True, True) strNoteID = nncElementType.GetNextNoteId(strNoteID) Next Exit Sub processerror: Print {Error } & Str(Err) & { in line } & Str(Erl) & { of Refresh Catalog - UpdateAllCategories: "} & Error$ & {"} Resume Next End Sub %REM Script Library: Declarations: %END REM Const TEMP_FOLDER = "C:\WINNT\Temp\" 'Assumes the user is running Windows 2000; change this value as appropriate Sub CreateElementVersion (Byval pstrReplicaID As String, Byval pstrNoteID As String, pndbCatalog As NotesDatabase) %REM This sub locates the most recent version, if any, of the design element matching the passed parameters. The first two parameters ensure uniqueness for the design element, the third allows multiple catalogs to be maintained. %END REM On Error Goto processerror 'Local Declarations 'Class Instances: Dim nsesCurrent As New NotesSession Dim ndbSource As New NotesDatabase( "", "" ) Dim nvwAllVersions As NotesView Dim nvwElementProfiles As NotesView Dim nncVersionElement As NotesNoteCollection Dim ndocMostRecentVersion As NotesDocument Dim ndocNextVersion As NotesDocument Dim ndocElementProfile As NotesDocument Dim nitmDXLSnapshot As NotesRichTextItem Dim nstrmOutput As NotesStream Dim ndxleCreateVersion As NotesDXLExporter 'Scalar Data Types: Dim intVersionNumber As Integer Dim strDefaultComment Dim strNoteID As String Dim strProfileKeys(1) As String Dim strVersionFile As String Dim strVersionID As String Dim strVersionSummary As String Dim varVersionID As Variant Set nvwAllVersions = pndbCatalog.GetView("VersionsByRepID") 'Used for locating the most recent version of this element Set nvwElementProfiles = pndbCatalog.GetView("DEByRepID") 'Used for locating the element profile Set ndocElementProfile = nvwElementProfiles.GetDocumentByKey(Evaluate({"} & pstrReplicaID & {":"} & pstrNoteID & {"}), True) If ndbSource.OpenByReplicaID( ndocElementProfile.ElementServer(0), pstrReplicaID ) Then 'Attempt to open the element's source database Print( "Source database " & ndbSource.Title & " was successfully opened" ) Else Print( "Unable to open source database" ) Exit Sub End If strVersionID = LSAtUnique() 'This function call returns the first string value in an array returned by an evaluation of @Unique strVersionFile = TEMP_FOLDER & strVersionID & ".dxl" 'Forms the temporary filename to be generated by the DXL Exporter Set nncVersionElement = ndbSource.CreateNoteCollection(False) 'Creates an empty Note collection Call nncVersionElement.BuildCollection 'Builds the collection to make it usable Call nncVersionElement.Add(pstrNoteID) 'Adds the design element to the collection Set nstrmOutput = nsesCurrent.CreateStream 'Create a Stream to store the DXL Call nstrmOutput.Open(strVersionFile) 'Create the temporary file Set ndxleCreateVersion = nsesCurrent.CreateDXLExporter(nncVersionElement,nstrmOutput) 'Create the DXL Exporter to convert the element Call ndxleCreateVersion.Process 'Convert the design element to DXL, output the DXL to the Stream, and write the Stream to the file Call nstrmOutput.Close 'Close the temporary file to allow it to be attached to the version document and later removed 'Determine the next version number (1 if none yet exist for this element) strProfileKeys(0) = pstrReplicaID strProfileKeys(1) = pstrNoteID Set ndocMostRecentVersion = nvwAllVersions.GetDocumentByKey(strProfileKeys, True) If ndocMostRecentVersion Is Nothing Then intVersionNumber = 1 strDefaultComment = "Baseline version for this element" 'Set a default comment if this is the first version Else intVersionNumber = Cint(ndocMostRecentVersion.GetItemValue("ElementVersionNumber")(0)) + 1 End If strVersionSummary = ndbSource.Title & " " & ndocElementProfile.ElementType(0) & " " & ndocElementProfile.ElementName(0) & " Version " & Cstr(intVersionNumber) Set ndocNextVersion = pndbCatalog.CreateDocument 'Create the version document Call ndocElementProfile.CopyAllItems(ndocNextVersion, True) 'Populate the element summary from the element profile With ndocNextVersion .Form = "ElementVersion" 'Override the copied form value 'Populate the version summary information (version number, ID, author, timestamp, and comment) .ElementVersionNumber = intVersionNumber .ElementVersionID = strVersionID .ElementVersionAuthor = nsesCurrent.CommonUserName .VersionTimeStamp = Now .ElementVersionComment = Inputbox("Enter an optional comment:", strVersionSummary, strDefaultComment) 'Store comments summarizing the changes in the version Set nitmDXLSnapshot = .CreateRichTextItem("DXLSnapshot") 'Create the rich text item to store the DXL (should not exist on the form, to protect the content) End With Call nitmDXLSnapshot.EmbedObject(EMBED_ATTACHMENT,"",strVersionFile) 'Attach the DXL Call ndocNextVersion.Save(True, True, True) 'Save the version document Print {Created version } & Cstr(intVersionNumber) & { of element } & pstrNoteID 'Display confirmation Kill strVersionFile 'Remove the temporary file Exit Sub processerror: Print {Error } & Str(Err) & { in line } & Str(Erl) & { of CreateElementVersion: "} & Error$ & {"} Resume Next End Sub Function LSAtUnique As String Dim varUniqueHolder As Variant varUniqueHolder = Evaluate({@Unique}) LSAtUnique = varUniqueHolder(0) End Function Sub RollbackToVersion (pndocVersion As NotesDocument) %REM This sub overwrites a design element with a previous version. It optionally calls CreateElementVersion to preserve the element's current state as a new version before overwriting it. pndocVersion is a handle on the document storing the previous version to which the element will be rolled back. %END REM On Error Goto processerror 'Local Declarations 'Class Instances: Dim nsesCurrent As New NotesSession Dim ndbSource As New NotesDatabase( "", "" ) Dim nstrmInput As NotesStream Dim ndxliImportVersion As NotesDXLImporter Dim ndocImportedNote As NotesDocument Dim nrtnDXLLocator As NotesRichTextNavigator Dim nembDXL As NotesEmbeddedObject 'Scalar Data Types: Dim strVersionFile As String Dim varDXLRTF As Variant If Not (Msgbox ("This will overwrite the current design element with version " & Cstr(pndocVersion.ElementVersionNumber(0)) & ". Do you wish to continue?", 292, "Overwrite Design Element?") = 6) Then Exit Sub If Msgbox ("Do you wish to save the current version before rolling back (strongly recommended)?", 36, "Save Current Version First?") = 6 Then CreateElementVersion pndocVersion.ElementReplicaID(0), pndocVersion.ElementNoteID(0), pndocVersion.ParentDatabase Else Print "Option to preserve current version declined." End If Set varDXLRTF = pndocVersion.GetFirstItem("DXLSnapshot") Set nrtnDXLLocator = varDXLRTF.CreateNavigator Set nembDXL = nrtnDXLLocator.GetFirstElement(8) If ndbSource.OpenByReplicaID( pndocVersion.ElementServer(0), pndocVersion.ElementReplicaID(0) ) Then 'Attempt to open the element's source database Print( "Source database " & ndbSource.Title & " was successfully opened") Else Print( "Unable to open source database" ) Exit Sub End If strVersionFile = TEMP_FOLDER & nembDXL.Source 'Forms the temporary filename Call nembDXL.ExtractFile(strVersionFile) 'Save the DXL to the temporary file Set nstrmInput = nsesCurrent.CreateStream 'Create a Stream to store the DXL Call nstrmInput.Open(strVersionFile) 'Open the temporary file Set ndxliImportVersion = nsesCurrent.CreateDXLImporter(nstrmInput, ndbSource) ndxliImportVersion.ReplicaRequiredForReplaceOrUpdate = False ndxliImportVersion.DesignImportOption = 6 'If the design element has been deleted, recreate, otherwise overwrite the existing Call ndxliImportVersion.Process 'Input the DXL to the Stream and convert the DXL to a design element Set ndocImportedNote = ndbSource.GetDocumentByID(ndxliImportVersion.GetFirstImportedNoteId) Call ndocImportedNote.Sign 'Sign the imported note Call ndocImportedNote.Save(True, True) 'Save the signature Call nstrmInput.Close 'Close the temporary file to allow it to be removed Kill strVersionFile 'Remove the temporary file Exit Sub processerror: Print {Error } & Str(Err) & { in line } & Str(Erl) & { of RollBackToVersion: "} & Error$ & {"} Resume Next End Sub 'Buttons: all require a Use statement referencing the above script library 'Take snapshot - form action button Dim nsesCurrent As New NotesSession Dim nuiwsCurrent As New NotesUIWorkspace Dim ndbCurrent As NotesDatabase Dim nuidCurrent As NotesUIDocument Dim ndocCurrent As NotesDocument Set ndbCurrent = nsesCurrent.CurrentDatabase Set nuidCurrent = nuiwsCurrent.CurrentDocument Set ndocCurrent = nuidCurrent.Document CreateElementVersion ndocCurrent.ElementReplicaID(0), ndocCurrent.ElementNoteID(0), ndbCurrent 'Take snapshot - view action button, increment version of all selected design elements at once Dim nsesCurrent As New NotesSession Dim ndbCurrent As NotesDatabase Dim ndcolSelected As NotesDocumentCollection Dim ndocElement As NotesDocument Set ndbCurrent = nsesCurrent.CurrentDatabase Set ndcolSelected = ndbCurrent.UnprocessedDocuments Set ndocElement = ndcolSelected.GetFirstDocument Do While Not (ndocElement Is Nothing) CreateElementVersion ndocElement.ElementReplicaID(0), ndocElement.ElementNoteID(0), ndbCurrent Set ndocElement = ndcolSelected.GetNextDocument(ndocElement) Loop 'Rollback - view action button for embedded versions view Dim nsesCurrent AsNew NotesSession Dim ndbCurrent As NotesDatabase Dim ndcolSelected As NotesDocumentCollection Dim ndocSelected As NotesDocument Set ndbCurrent = nsesCurrent.CurrentDatabase Set ndcolSelected = ndbCurrent.UnprocessedDocuments Set ndocSelected = ndcolSelected.GetFirstDocument RollbackToVersion ndocSelected