Author: Kyle

  • SharePoint List Item Loading Page — Query from Multiple Lists

    SharePoint List Item Loading Page — Query from Multiple Lists

    This blog post covers how to implement a simple, yet devilishly effective mutiple-list query redirect page using SPServices. If you have not yet explored how SPServices spservices_logocan expand your current SharePoint solutions, you should definitely check the project out on CodePlex!

    A client recently required a single landing page (a “loading” page, if you will) from which a list item will open based an a URL parameter, where the parameter matches a column value in the list item. The caveat though, is that the list item could exist across any of six individual (and very large) custom lists, which do not necessarily have the same structure. The lists are all closely related and represent the movement of work items through a tightly-controlled business process… so the item could change locations throughout the day.

    For the sake of clarity, let’s call the work items “documents” to represent the business use-case. Documents have a unique identifying number that generates when an initial request arrives in one department. This DocID then represents the easiest way for an end-user to check on the status of the the request, the resulting document, or both, at any point in the business process.

    List A List B
    Initial Requests Archived Requests
    DocID (Internal list item ID) DocID (Number column)
    Unique Unique
    List C List D List E List F
    Submitted Documents In Review Documents Approved Documents Obsolete Documents
    DocID (Number column) DocID (Number column) DocID (Number column) DocID (Number column)
    Unique Unique Unique Duplicates Possible

    This “landing” page needs to query these lists in a particular order, taking into account the current user’s permissions (skipping over lists to which he/she does not have read rights), and loading the document or request as soon as the query finds a match.

    So, how best to approach the issue of querying for the location of the document based on this DocID?

    Out-of-the-box, SharePoint offers the standard list view web part that can be connected to URL filter parameters, but I needed to display the list item’s view form directly (cutting out that extra click). Theoretically, you could connect a view form web part to the URL parameter, but in this case it would be very sloppy to repeat this six times on the same page to cover all the lists.

    Third-party, the challenge reduces to a trivial implementation using SPServices JQuery library for SharePoint 2010; fortunately; this was already in use elsewhere in the site collection.

    The .aspx loading page itself contains a call to the SPServices SPGetQueryString function, to parse the URL parameter named “doc“…

    var queryStringValues = $() .SPServices.SPGetQueryString();
    var strDocId = queryStringValues["DOC"];
    //double-check for lowercase parameter name:
    if(!strDocId) strDocId = queryStringValues["doc"]

    …the SPServices getListItems calls and logic to query the six lists…

    	var cQueryOptions = "<QueryOptions><ExpandUserField>FALSE</ExpandUserField></QueryOptions>" ;
    	var url;
    	var foundFlag = false;
    
    	function queryListA() {
    		//call GetListItems
    		//set url=<URL OF LIST A> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListB() {
    		//call GetListItems
    		//set url=<URL OF LIST B> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListC() {
    		//call GetListItems
    		//set url=<URL OF LIST C> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListD() {
    		//call GetListItems
    		//set url=<URL OF LIST D> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListE() {
    		//call GetListItems
    		//set url=<URL OF LIST E> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListF() {
    		//call GetListItems
    		//set url=<URL OF LIST F> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	$(document).ready(function() {
    		if(strDocId) {
    			if(strDocId=='0') {
    				alert("The document ID provided (\"0\") is not valid. Please try again."
    				foundFlag = true;
    				window.location = URL TO HOMEPAGE;
    			}
    
    			if(!foundFlag) queryListE();
    			if(!foundFlag) queryListD();
    			if(!foundFlag) queryListC();
    			if(!foundFlag) queryListF();
    			if(!foundFlag) queryListA();
    			if(!foundFlag) queryListB();
    		} else {
    			alert("The link is invalid or you have accessed this page in an unsupported manner. Please check the link and try your request again.");
    			window.location = URL TO HOMEPAGE;
    		}
    	});
    

    GEARS_AN

    …and a simple “loading” message that displays to the user until the query completes (making clever use of the native SharePoint loading graphic gears_an.gif… which everyday users of SharePoint will readily recognize as a loading signal). I also styled the container divs using CSS3PIE so I could be sure this loading page has a professional finish regardless of the user’s browser version.

     

    <div class="LoadingMsgMain">
    <div class="LoadingMsgTitle"> YOUR SITE TITLE HERE</div>
    <div style="text-align:center; font-size:large; font-face:Calibri; padding:20px;">Please wait the system prepares your document...
    <br /><br />
    <img src='/_layouts/images/gears_an.gif' style="border:none;" /></div>
    </div>

    The page then redirects to the display form of the matching list item, or displays an error message if no match found or an invalid URL parameter is provided.

    Full page source below.

    <html>
    <head>
    <title>YOUR PAGE TITLE HERE</title>
    
    <!-- Include the SPServices and Jquery libraries from a local site library (or just reference them from a CDN of your choice) -->
    <script language="javascript" type="text/javascript" src="/mySiteCodeLibrary/jquery-1.11.0.min.js"></script>
    <script language="javascript" type="text/javascript" src="/mySiteCodeLibrary/jquery.SPServices-2014.01.min.js"></script>
    
    <script type="text/javascript">
    	var queryStringValues = $() .SPServices.SPGetQueryString();
    	var strDocId = queryStringValues["DOC"];
    	//double-check for lowercase parameter name:
    	if(!strDocId) strDocId = queryStringValues["doc"]
    	var cQueryOptions = "<QueryOptions><ExpandUserField>FALSE</ExpandUserField></QueryOptions>" ;
    	var url;
    	var foundFlag = false;
    
    	function queryListA() {
    		//call GetListItems
    		//set url=<URL OF LIST A> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListB() {
    		//call GetListItems
    		//set url=<URL OF LIST B> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListC() {
    		//call GetListItems
    		//set url=<URL OF LIST C> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListD() {
    		//call GetListItems
    		//set url=<URL OF LIST D> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListE() {
    		//call GetListItems
    		//set url=<URL OF LIST E> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	function queryListF() {
    		//call GetListItems
    		//set url=<URL OF LIST F> + "/DispForm.aspx?ID=" + MATCHING ITEM ID
    		//inside completefunc, set foundFlag=true and window.location=url;
    	}
    
    	$(document).ready(function() {
    		if(strDocId) {
    			if(strDocId=='0') {
    				alert("The document ID provided (\"0\") is not valid. Please try again."
    				foundFlag = true;
    				window.location = URL TO HOMEPAGE;
    			}
    
    			if(!foundFlag) queryListE();
    			if(!foundFlag) queryListD();
    			if(!foundFlag) queryListC();
    			if(!foundFlag) queryListF();
    			if(!foundFlag) queryListA();
    			if(!foundFlag) queryListB();
    		} else {
    			alert("The link is invalid or you have accessed this page in an unsupported manner. Please check the link and try your request again.");
    			window.location = URL TO HOMEPAGE;
    		}
    	});
    </script>
    </head>
    
    <body>
    <div class="LoadingMsgMain">
    <div class="LoadingMsgTitle"> YOUR SITE TITLE HERE</div>
    <div style="text-align:center; font-size:large; font-face:Calibri; padding:20px;">Please wait the system prepares your document...
    <br /><br />
    <img src='/_layouts/images/gears_an.gif' style="border:none;" /></div>
    </div>
    </body>
    </html>
    

     

     

  • SharePoint Single-Value Lookup Column Index (Strange Behavior)

    Today’s post continues my previous discussion of column index issues, but this time we focus solely on the indexing of a single-value lookup column and some of the strange behavior that indexing can produce when the row contains a blank value.

    Keep in mind that lookup columns set to allow multiple selections are not supported by Microsoft in SharePoint 2010.

    Diagnosis

    I had a production list of “requests” with a single-value lookup column into another production list of department employees. The lookup column was thus used to assign a request to an owner. Now, you might be wondering why I would simply not just use a column of type person, and I would say to you that you’ve asked a perfectly valid question. The lookup table, if you will, allows me to track historical owners that have moved on from the department, without worrying about data loss as requests are updated. The lookup table also enables me to store additional information on the owners that can easily be retrieved from within workflows.

    So with this production list approaching the dreaded 5,000 item list view threshold, I decided it was time to properly address column indexing. One of my changes included indexing this single-value lookup “Owner” column. Now, in most cases this would be perfectly fine, but keep in mind (as I initially did not), that new, incoming requests do not have an owner until assigned. This means that the now indexed lookup column has (albeit very few) blank values.

    SharePoint will accept the column index and go about daily operations as if everything is fine, but you may notice something strange when using column filters in your list views. When you select (blanks) as the filter on this “Owner” column, you would normally expect to see all list items that do not have an owner assigned. But instead, the list view shows you nothing (as if to say there are no items with a NULL owner).

    Solution

    This behavior persists until you remove the column index. Instantly, the column filter (blanks) will work as you would expect. Add the column index back, and the issue is reproducible.

    Strange, but nonetheless manageable, when you remember that column indexing simply does not play well with blank values. Lesson learned!

  • SharePoint List Web Services via PowerShell

    The Project: Migrating Data from Legacy System to SharePoint

    I recently completed a data migration project to migrate an aging Domino database to a custom site collection in SharePoint 2010. The data itself was processed in two stages: first via LotusScript to extract it from the Domino database, and then via built-in SharePoint web services to import it into the new SharePoint site collection.

    Environment Limitations

    The caveat in my scenario, however, is that my highest level of access to the SharePoint environment was site collection administrator. I did not have farm administrator rights, and requesting temporary rights was out of the question.

    This ruled out the use of the SharePoint server-side object model, as well as the SharePoint Management Console cmdlets via PowerShell, which was by far the most favorable and straightforward approach to importing the data. This also ruled out several third-party tools, but those would have added additional cost to a project that I was already managing with zero budget.

    Instead, I consumed the out-of-the-box SharePoint web services via PowerShell to process my data using the limited tools available. This post describes how I utilized a few list services to update list items and upload file attachments.

    The Data to Import

    I constructed new SharePoint lists to mirror the Domino database schema and then imported all of my text-based data into the new lists using datasheet views. This left me with skeleton list items that were missing dynamic content from the Domino database (i.e. rich-text data and file attachments). These items were exported from Domino using LotusScript into a directory structure based on the internal unique Domino document ID.

    To add a layer of complexity, the Domino data had to be merged with a few thousand existing list items in SharePoint. In some cases, there were duplicate items, so my PowerShell script logic was designed to handle those duplicates in a graceful manner (by merging fields and archiving one of the duplicates). To handle the tens of thousands of exported file attachments from the Domino database, I designed script logic to re-attach them to the parent items in the new SharePoint site lists after successfully matching them based on the Domino document ID. I used PowerShell’s exceedingly simple filesystem cmdlets to enumerate the Domino export directory and match the name of each subfolder (the Domino document ID) to an imported list item on the SharePoint list. Once matched, the files in that subfolder were attached to the SharePoint list item and some fields updated to indicate that the item has been processed already by the script.

    My Toolbox

    • SharePoint 2010 Web Services
    • PowerShell 3.0 (or higher)
    • PowerGUI 3.8.0.129 – a lightweight, free GUI editor for PowerShell scripts. I used this instead of the PowerShell ISE for its advanced debugging features. These were essential to my success with digesting the SharePoint web services, as they allow you to visualize the web service calls and returns in realtime.
    • CAML Designer 2013 – an excellent, lightweight tool for building CAML queries from existing SharePoint data. I used the 2013 version since it is backward compatible with 2010. In a few short clicks, you can connect the Designer to your SharePoint site, select your target list, enter the query conditions, and like magic, the CAML is generated for you in a variety of formats.

    The Script

    Assumptions

    I make certain assumptions about the reader’s knowledge of PowerShell and general SharePoint development, given that you probably would not be interested in my write-up unless you have a reason to be using these tools together.

    First-things-first: Framework (My Scripting Style), Variables, and Definitions

    I will openly admit that I may carry forward some well-worn habits from my past scripting experience with VBScript, shell, batch, LotusScript, etc. If you entered the scripting world starting with PowerShell, please try to overlook these.

    Since I am a faithful advocate of debugging options in my major scripts, I start this one with some PowerShell logic to grab a debug parameter from the command-line, or default to no “debugging” otherwise. As you will see, debugging is lightly implemented in my code samples, but was more heavily (and specifically) used in the production version. Customize or eliminate this completely, as needed.

    #*** PARAMETERS ***
    #pass a debug flag to toggle additional command-line output while testing the script
    param (
    [Int]$debug = 0
    )
    #******************

    I defined some constant values near the top of my script so I can easily re-use the code should I need only to point it to a different environment.

    #*** CONSTANTS ***
    #Establish "test" and "prod" variables to use in conjunction with the debug parameter above
    if($debug) {
    # Define the source network location (UNC Path):
    $directory = "\\network\path\test"
    # Define the target SharePoint site collection:
    $uri = "http://url/testsite/_vti_bin/lists.asmx?wsdl"
    # Define the target SharePoint list in above site collection:
    $listName = "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
    } else {
    $directory = "\\network\path\prod"
    $uri = "http://url/prodsite/_vti_bin/lists.asmx?wsdl"
    $listName = "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
    }
    #******************

    I work with a team not entirely composed of scripters, so for the sake of clarity I always declare my global variables near the top of the script. Note, though, that PowerShell does not require this.

    #*** GLOBALS ***
    #(declared here for clarification)
    $IDMigrated
    $IDExisting
    $totalPSProcessed
    $totalFilesAttached
    $totalMerged
    #*** INCLUDES ***
    Add-Type -AssemblyName System.Web
    #*****************

    Script Functions:

    function ResetXML() {
    # Resets the XML query parameters
    $global:query = $null
    $global:qxml = $null
    $global:viewFields = $null
    $global:vfxml = $null
    }
    
    function BlankQueryElement() {
    # Builds the base XML query elements (default values are blank/null)
    $global:xmlDoc = new-object System.Xml.XmlDocument
    $global:viewName = "" # Default
    $global:list = $null
    $global:query = $global:xmlDoc.CreateElement("Query")
    $global:viewFields = $global:xmlDoc.CreateElement("ViewFields")
    
    # Define the query options:
    # (presence required in the XML, but not used in this case)
    $global:queryOptions = $global:xmlDoc.CreateElement("QueryOptions")
    $global:rowLimit = "1"
    }
    
    function ExecuteQuery() {
    # Queries the SharePoint list service and returns the matching list item ID
    # (returns NULL if no match or connection error)
    if($global:service -ne $null){
    try{
    $global:list = $global:service.GetListItems($global:listName, "", $global:query, $global:viewFields, $global:rowLimit, $queryOptions, "")
    MyOutput ("Query success on ID " + $list.data.row | Select-Object -ExpandProperty ows_ID)
    return $list.data.row | Select-Object -ExpandProperty ows_ID
    }
    catch{
    $global:matchingTitle = $null
    MyOutput ("Failed to query the SharePoint list: " + $global:query.OuterXml)
    $tempStr = "Failed to query the SharePoint list: " + $global:query.OuterXml + " - " + $_
    if($debug) {
    Write-Error -Message $tempStr
    Throw "Failed to query the SharePoint list." }
    return $null
    }
    } else {
    Throw "SharePoint web service connection ERROR!"
    }
    }
    
    function AddAttachment($aID) {
    #Adds a file attachment (global variable) to the given list item (ID passed to function)
    $tempReturnValue
    # Set the XML content:
    $global:batchelement.innerxml = $global:xml
    
    try {
    $tempReturnValue = $global:service.AddAttachment($global:listName, $aID, $global:file.ToString(), $global:buffer)
    MyOutput ("Attach success!`t" + $global:notesID + "`t" + $aID + "`t" + $global:attachFileName)
    $global:totalFilesAttached++
    }
    catch {
    MyOutput ("Failed to attach file:" + $file.ToString() + " " + $tempReturnValue)
    MyOutput ($global:notesID + "`t" + $matchingID + "`t" + $global:matchingTitle + "`t" + $global:attachFileName + "`tFAILED")
    write-error $_ -erroraction:'SilentlyContinue'
    }
    
    return $tempReturnValue
    }
    
    function UpdateListItemField($ID,$be) {
    # This function will update the SharePoint list item with given ID using the passed XML batch element
    # Call the web service:
    try {
    $global:service.updatelistitems($global:listName, $be)
    MyOutput ("Success: updatelistitems on ID " + $ID + ": " + $be.InnerXml)
    }
    catch {
    MyOutput ("Failed: updatelistitems on ID " + $ID + " with error: " + $ndRetUpdate.ChildNodes[0].InnerText)
    write-error $_ -erroraction:'SilentlyContinue'
    }
    }
    
    function MyOutput($outStr) {
    #A customized output function that can change logging behavior  based on the debug parameter
    If($debug) {
    Write-Host $outStr
    } else {
    Write-Host $outStr
    Write-Output $outStr | Out-File -FilePath:"MyOutputLog.txt" -Append:$true -Force:$true
    }
    }

    Body of Script

    $totalFilesAttached = 0
    MyOutput " "
    MyOutput "****** NEW SCRIPT ITERATION ******"
    if($debug) {MyOutput "*** DEBUG MODE ***"}
    
    #*** CONNECT WITH SHAREPOINT WEB SERVICES ***
    # (uses credentials of current user)
    $service = $null
    try{
    $service = New-WebServiceProxy -Uri $uri  -Namespace SpWs  -UseDefaultCredential
    MyOutput "Connected to SharePoint web service!"
    }
    catch{
    MyOutput "Failed to connect to SharePoint web service!"
    Exit
    }
    
    #*** ENUMERATE THE FOLDERS ***
    # Get all subfolder names from source location:
    $foldernames = Get-ChildItem -Path $directory | Select-Object -ExpandProperty Name
    If(!$foldernames) {
    MyOutput "ERROR: UNC path not available!"
    Exit
    }
    Else {
    MyOutput ("Total number of folders to process: " + $foldernames.Count)
    }
    
    If($debug) {
    MyOutput "List of Domino IDs:"
    ForEach($dominoID in $foldernames) {
    MyOutput $dominoID
    }
    }
    
    $dominoID = ""
    ForEach($dominoID in $foldernames) {
    MyOutput ("Processing notesID: " + $dominoID)
    
    # Reset XML parameters:
    ResetXML
    # Generate new XML query element:
    BlankQueryElement
    
    # Customize the query:
    # Look for a match on "MigrateID", and "PSProcessed" equal to FALSE
    # (PSProcessed set to true if row already processed by this script)
    $qxml = "&amp;amp;lt;Where&amp;amp;gt;&amp;amp;lt;And&amp;amp;gt;&amp;amp;lt;Eq&amp;amp;gt;&amp;amp;lt;FieldRef Name='MigrateID' /&amp;amp;gt;&amp;amp;lt;Value Type='Text'&amp;amp;gt;" + $dominoID + "&amp;amp;lt;/Value&amp;amp;gt;&amp;amp;lt;/Eq&amp;amp;gt;&amp;amp;lt;Eq&amp;amp;gt;&amp;amp;lt;FieldRef Name='PSProcessed' /&amp;amp;gt;&amp;amp;lt;Value Type='Integer'&amp;amp;gt;0&amp;amp;lt;/Value&amp;amp;gt;&amp;amp;lt;/Eq&amp;amp;gt;&amp;amp;lt;/And&amp;amp;gt;&amp;amp;lt;/Where&amp;amp;gt;"
    $query.set_InnerXml($qxml)
    
    # Define the item field(s) to retrieve:
    $vfxml = ""
    $viewFields.set_InnerXml($vfxml)
    
    # Execute the query and get the matching list ID:
    $matchingID = ExecuteQuery
    If($matchingID) {
    $matchingTitle = $list.data.row | Select-Object -ExpandProperty ows_Title
    $matchingSubtitle = $list.data.row | Select-Object -ExpandProperty ows_Subtitle
    $matchingAttachments = $list.data.row | Select-Object -ExpandProperty ows_Attachments
    
    # Re-requery the list to find any existing item with the same CR ID:
    ResetXML
    BlankQueryElement
    
    # Look for a match on "CR ID", and "PSProcessed" equal to FALSE
    $ImportID = $ImportID.Substring(0,4)
    $qxml = "&amp;amp;lt;Where&amp;amp;gt;&amp;amp;lt;And&amp;amp;gt;&amp;amp;lt;And&amp;amp;gt;&amp;amp;lt;Eq&amp;amp;gt;&amp;amp;lt;FieldRef Name='ImportID' /&amp;amp;gt;&amp;amp;lt;Value Type='Integer'&amp;amp;gt;" + $ImportID + "&amp;amp;lt;/Value&amp;amp;gt;&amp;amp;lt;/Eq&amp;amp;gt;&amp;amp;lt;IsNull&amp;amp;gt;           &amp;amp;lt;FieldRef Name='MigrateID' /&amp;amp;gt;&amp;amp;lt;/IsNull&amp;amp;gt;&amp;amp;lt;/And&amp;amp;gt;&amp;amp;lt;Eq&amp;amp;gt;&amp;amp;lt;FieldRef Name='PSProcessed' /&amp;amp;gt;&amp;amp;lt;Value Type='Integer'&amp;amp;gt;0&amp;amp;lt;/Value&amp;amp;gt;&amp;amp;lt;/Eq&amp;amp;gt;&amp;amp;lt;/And&amp;amp;gt;&amp;amp;lt;/Where&amp;amp;gt;"
    $query.set_InnerXml($qxml)
    
    # Define the item field(s) to retrieve (all):
    $vfxml = ""
    $viewFields.set_InnerXml($vfxml)
    
    # Execute the query and get the matching list ID:
    $IDExisting = ExecuteQuery
    }
    
    If($IDExisting) {
    $existingTitle = $list.data.row | Select-Object -ExpandProperty ows_Title
    $existingSubtitle = $list.data.row | Select-Object -ExpandProperty ows_Subtitle
    $existingAttachments = $list.data.row | Select-Object -ExpandProperty ows_Attachments
    $existingMigrateID = $list.data.row | Select-Object -ExpandProperty ows_MigrateID
    }
    
    If($matchingID -and -not $matchingPSProc) {
    MyOutput ("The matching migrated item ID is " + $matchingID + ' - ' + $matchingTitle)
    if($IDExisting) {MyOutput ("The matching existing item ID is " + $IDExisting + ' - ' + $existingTitle)}
    
    #Add the files from the matching subfolder as attachments on the list item:
    $tempPath = $directory + "\" + $dominoID + "\"
    $filenames = Get-ChildItem -Path $tempPath | Select-Object -ExpandProperty Name
    Write-Host $filenames
    $totalFiles = $filenames.Count
    
    # Get name attribute values (guids) for list and view:
    $ndlistview = $service.getlistandview($listname, "")
    $strlistid = $ndlistview.childnodes.item(0).name
    $strviewid = $ndlistview.childnodes.item(1).name
    
    # Create an xmldocument object and construct a batch element and its attributes.
    $xmldoc = new-object system.xml.xmldocument
    
    # note that an empty viewname parameter causes the method to use the default view
    $batchelement = $xmldoc.createelement("Batch")
    $batchelement.setattribute("OnError", "Continue")
    $batchelement.setattribute("ListVersion", "1")
    $batchelement.setattribute("ViewName", $strviewid)
    
    # Methods for batch call in CAML:
    # To update or delete, specify the id of the existing item.
    # To update or add, specify the target column and the target value.
    $xml = ""
    
    # Grab the target attachment file (in raw bytes):
    ForEach($attachFileName in $filenames) {
    $file = Get-Item ($tempPath + $attachFileName)
    $stream = $file.OpenRead()
    $buffer = New-Object byte[] $stream.length
    [void]$stream.Read($buffer, 0, $stream.Length)
    
    # *********************************************** #
    # Attempt to attach file on migrated Domino item:
    AddAttachment $matchingID
    # *********************************************** #
    
    if($IDExisting) {
    # Attempt to attach files on original (existing) SharePoint item:
    $ndRetAttach = AddAttachment $IDExisting
    }
    }
    MyOutput ("  Results: " + $totalFilesAttached + " of " + $totalFiles)
    
    if($IDExisting) {
    #**************************************************************
    # With attachments complete, now verify and update fields on
    # existing item, as needed (only when blank):
    #**************************************************************
    if(!$existingSubtitle) {
    $xml = "&amp;amp;lt;Method ID='1' Cmd='Update'&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='ID'&amp;amp;gt;$IDExisting&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='Subtitle'&amp;amp;gt;$matchingSubtitle&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;/Method&amp;amp;gt;"
    $batchelement.innerxml = $xml
    UpdateListItemField  $IDExisting $batchelement
    }
    if(!$existingMigrateID ) {
    $xml = "&amp;amp;lt;Method ID='1' Cmd='Update'&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='ID'&amp;amp;gt;$IDExisting&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='MigrateID'&amp;amp;gt;$dominoID&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;/Method&amp;amp;gt;"
    $batchelement.innerxml = $xml
    UpdateListItemField  $IDExisting $batchelement
    }
    }
    
    #**************************************************************
    # Now that actions are complete, update PSProcessed column on:
    # 1) matching migrated Notes item
    # 2) matching existing SharePoint item (if it exists)
    #**************************************************************
    if($IDExisting) {
    #Create batch element that sets migrated Notes item to Obsolete:
    $xml = "&amp;amp;lt;Method ID='1' Cmd='Update'&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='ID'&amp;amp;gt;$matchingID&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='PSProcessed'&amp;amp;gt;1&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='Status'&amp;amp;gt;Archived&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='History'&amp;amp;gt;This item was migrated from Domino and merged with an existing SharePoint item.&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;/Method&amp;amp;gt;"
    
    #And increment the merged document counter:
    $totalMerged++
    } else {
    #Otherwise, the migrated Notes item remains at current status:
    $xml = "&amp;amp;lt;Method ID='1' Cmd='Update'&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='ID'&amp;amp;gt;$matchingID&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='PSProcessed'&amp;amp;gt;1&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='History'&amp;amp;gt;This item was migrated from Domino.&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;/Method&amp;amp;gt;"
    }
    # Set the XML content:
    $batchelement.innerxml = $xml
    UpdateListItemField $matchingID $batchelement
    
    #And increment the total PSProcessed counter:
    $totalPSProcessed++
    
    if($IDExisting) {
    #If a matching item exists, update it too:
    $xml = "&amp;amp;lt;Method ID='1' Cmd='Update'&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='ID'&amp;amp;gt;$IDExisting&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;Field Name='PSProcessed'&amp;amp;gt;1&amp;amp;lt;/Field&amp;amp;gt;" +
    "&amp;amp;lt;/Method&amp;amp;gt;"
    # Set the XML content:
    $batchelement.innerxml = $xml
    # Reset the return code:
    $ndRetUpdate = $null
    
    UpdateListItemField $IDExisting $batchelement
    }
    }
    ElseIf($matchingID -and $matchingPSProc) {
    MyOutput "Matching item " + $matchingID + " already processed by script."
    }
    Else {
    If($debug) {MyOutput "No matching item."}
    }
    }
    
    #output final stats:
    MyOutput ("Total list items processed by script: " + $totalPSProcessed)
    MyOutput ("Total list items archived via merge process: " + $totalMerged)
    MyOutput ("Total files attached to list items: " + $totalFilesAttached)
    MyOutput ("Script processing complete.")
  • Exception from HRESULT: 0x80131904 (Column Index Issue)

    A frequently-accessed production list suddenly started exhibiting strange behaviors as users (and automated workflows!) attempted to edit existing items. Users could add new items and delete existing items from the list without issue, using both datasheet views and the list forms. However, if users tried to update existing items (evenly a newly created item), SharePoint would exhibit the following errors:

    When editing in a datasheet view:

    "An unexpected error has occurred. Changes to your data cannot be saved. For this error, you can retry or discard your changes."
    “An unexpected error has occurred. Changes to your data cannot be saved. For this error, you can retry or discard your changes.”

    When editing in the standard (non-customized) list form OR customized InfoPath edit form:

    "The form cannot be submitted. Exception from HRESULT: 0x80131904 An entry has been added to the Windows event log of the server."
    “The form cannot be submitted. Exception from HRESULT: 0x80131904 An entry has been added to the Windows event log of the server.”

    This behavior did not replicate onto any other list in the site collection, but remained localized to this single, critical list (naturally).

    The Investigation

    Since the errors occurred in both datasheet views and the list edit forms, the root cause could not be limited to InfoPath forms services (since the list in question uses custom InfoPath display, edit, and new forms).

    Strangely, the error message in datasheet view does not provide a correlation ID. Fortunately, the more detailed error message in form mode does provide a correlation ID. Using those correlation IDs, our troubleshooting research uncovered several frontend server log entries that harken to SQL server exceptions. At this point, the server logging level was set to HIGH.

    High Batchmgr Method error. Errorcode: 0x564aa500. Error message: The operation failed because an unexpected error occurred. (Result Code: 0x80131904)

    Okay, we’ve found the matching error code, now let’s look a bit deeper…

    High SqlError: 'The variable name '@CmpIndexValue1' has already been declared. Variable names must be unique within a query batch or stored procedure.' Source: '.Net SqlClient Data Provider' Number: 134 State: 1 Class: 15 Procedure: '' LineNumber: 3 Server: 'XXXXXXXXXXX-REDACTED-XXXXXXXXXXXXX'&amp;lt;em&amp;gt;
    Critical Unknown SQL Exception 134 occurred. Additional error information from SQL Server is included below.The variable name '@CmpIndexValue1' has already been declared. Variable names must be unique within a query batch or stored procedure.
    System.Data.SqlClient.SqlException: The variable name '@CmpIndexValue1' has already been declared. Variable names must be unique within a query batch or stored procedure. at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean
    System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection) at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj) at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj) at System.Data.SqlClient.SqlDataReader.ConsumeMetaData() at System.Data.SqlClient.SqlDataReader.get_MetaData() at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString) at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async) at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result) at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method) at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method) at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior) at Microsoft.SharePoint.Utilities.SqlSession.ExecuteReader(SqlCommand command, CommandBehavior behavior, SqlQueryData monitoringData, Boolean retryForDeadLock)

    Bingo! So in the heat of the moment, we surmised that this SQL exception might be caused by a database error, or worse, database corruption (a number of forum posts across the internet seem to suggest this idea). A colleague ran some queries on the SQL server to try to locate this “CmpIndexValue1” thinking that it might be part of stored procedure, but to no avail. It is here that we engaged Microsoft support to investigate further.

    After reproducing the issue with Microsoft support, we sent off a VERBOSE log dump for analysis. A short while later, the technician narrowed the possible culprits down to one (or more) of the list column indices, which are defined in the list settings.

    Root Cause

    System.Data.SqlClient.SqlException: The variable name '@CmpIndexValue1' has already been declared. Variable names must be unique within a query batch or stored procedure.

    List Indices

    We then proceeded to examine each of the six indexes I had configured when the list was originally created. From the six, only one was a compound index of a number column (primary) and date column (secondary). Based on the log entries that reference “@CmpIndexValue1” this compound list index must be the source of our troubles.

    First, we verified that this type of compound index is officially supported per the SharePoint documentation on TechNet. Second, we considered the data values stored in each column. The number column contained values ranging from 0 to 99,999 (integers only), and the date column contained only date values (no time per the column settings). The latter detail was less important, as Microsoft supports both date and time values when indexing the column. Values in either column were not unique, and duplicates were definitely present in the list.

    Final Resolution

    With the suspected index in our cross-hairs, I deleted the compound index and tried to edit some list items. Both avenues worked flawlessly, via datasheet and via the list forms. I then recreated the compound index, and the edit issue reappeared on cue. Since the compound index was created to meet a future business requirement, the support technician suggested I recreate the index, but switch the primary and secondary columns. Unfortunately, selecting the date column as primary then disables the selection of secondary column. As a workaround, I created two simple indexes, one for the number column and one for the date column. This did not cause the edit issue to return, so we considered the case resolved.

    At best, we concluded that the compound index is fully supported, but somewhere among the thousands of data values in these two columns, SharePoint did not tolerate one or more of them while building the compound index. At the end of the day, I was relieved to have a business-critical list fully functional again, and only mildly disappointed to have to adjust some list views in preparation for the inevitable march above and beyond 5,000 items.