Showing posts with label web services. Show all posts
Showing posts with label web services. Show all posts

Friday, 4 November 2011

C# function to get the Content Type GUID from a Sharepoint list using web services

Simple helper function to get the GUID of a list from Sharepoint web services.

Note that in the code below I have a class level object pointing to the Lists.asmx web service called m_listService already established , but I have left in the code to establish this connection in the comments as an example.

string GetListContentTypeGuid(string listName, string listID, string contentTypeName)
        {
            string defaultGuid = "";
            
            try
            {
                
                //ListsService.Lists listService = new ListsService.Lists();
                //listService.Credentials = System.Net.CredentialCache.DefaultCredentials;

                //XmlNode ndList = listService.GetList(fileInfo.m_listInfo.m_listName);
                //XmlNode ndVersion = ndList.Attributes["Version"];

                XmlNode contentTypes = m_listService.GetListContentTypes(listID, "anything");

                // Loop through the fields
                foreach (XmlNode node in contentTypes)
                {
                    System.Diagnostics.Debug.Print(node.Attributes["Name"].Value.ToString());
                    if (node.Attributes["Name"].Value.ToString() == contentTypeName)
                    {
                        defaultGuid = node.Attributes["ID"].Value.ToString();
                        break;
                    }
                }
                
            }
            catch (Exception ex)
            {

                throw new Exception("ERROR: Reading content types from target site.\r\n"
                    + ex.Message + "\r\nDetails: " + ex.InnerException + "\r\n" +
                    "Check the settings file to ensure that the list settings match the target site.", ex);

            }
            return defaultGuid;
        }

Wednesday, 20 July 2011

Sharepoint 2010 managed meta-data taxonomy field data using web services

Sharepoint 2010 includes a very useful new concept: managed meta-data. This allows you to build taxonomic hierarchies, and provide document routing based on the meta-data.

Unfortunately, it seems that using this field type using a web service call is extremely complicated, and hardly seems to be documented at all (this is so typical of Sharepoint stuff...).  Microsoft have given us a new web service class ("taxonomyclientservice") but there hardly any documentation on it all (http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.taxonomy.webservices.taxonomyclientservice.aspx).



Here is an example that I have managed to get working (after MUCH trial and error) that will hopefully help you to do the following:
  1. opens a list using the Lists web service, 
  2. iterates through the fields in the default content type for the list (you can remove this bit and just iterate through the fields in the list if you like),
  3. if it finds a "TaxonomyFieldType" field, it then gets the "Shared Service ID" (GUID of managed meta-data service) and Term Set ID (GUID of term set defined on this field) required to run the Taxonomy web service call
  4. sets up the CAML to run the Taxonomy web service call,
  5. retrieves the taxonomy data for that field in XML.  
 I do not try to analyse what the taxonomy XML means - that is for another day!

Note that all my web service proxies are called "ListService" and "Taxonomys".

//get list data from webservice
ListService.Lists listService = new ListService.Lists();
// Use your own list name
XmlNode list = listService.GetList("My List Name");

// Let's have a look at it
System.Diagnostics.Debug.Print(list.OuterXml);

// Get the content types for this list
//Get our defined content type (see previous post on how to do this - you need your own values here!)
string contentTypeGuid = GetListContentTypeGuid("My List Name", "{GUID}", "My Content Type");

// Now get content type definition 
XmlNode contentType = m_listService.GetListContentType(mylistinfo.m_listName, contentTypeGuid);

//Get the fields from the content type
XmlNodeList nodes = RunXPathQuery(contentType, "//sp:Field");

// Loop through the fields in the content type
foreach (XmlNode node in nodes)
{
 // This stuff just sets up some variables based on the content type and skips over hidden fields
 string Name = node.Attributes["Name"].Value;
 if (Name == "ContentType") continue;
 
 string ID = node.Attributes["ID"].Value;

 if (string.IsNullOrEmpty(ID) || string.IsNullOrEmpty(Name))
     continue;

 bool hidden = false;
 bool ReadOnly = false;
 try { hidden = Convert.ToBoolean(node.Attributes["Hidden"].Value); }
 catch { }
 try { ReadOnly = Convert.ToBoolean(node.Attributes["ReadOnly"].Value); }
 catch { }

 if (hidden || ReadOnly)
     continue;

 string ShowInFileDlg = "";
 try { ShowInFileDlg = node.Attributes["ShowInFileDlg"].Value; }
 catch { }
 if (ShowInFileDlg == "FALSE") continue;


 string StaticName = node.Attributes["StaticName"].Value;
 string DisplayName = node.Attributes["DisplayName"].Value;

 
 // Now we can check the "Type" attribute
 string FieldType = node.Attributes["Type"].Value;

 if (FieldType == "TaxonomyFieldType")
 {
     // WE HAVE A TAXONOMY FIELD!!!

     // Lets get the shared service ID and the termset ID from teh List schema
     string sharedServiceIds = "";
     string termSetId = ""; 
     // jump a few nodes to get the correct bit of the schema (included for clarity)
     XmlNode childNode =  node.ChildNodes[1];
     XmlNode termNodes = childNode.ChildNodes[0];

     //Loop through these nodes until we find the information we need
     foreach (XmlNode term in termNodes.ChildNodes)
     {
         System.Diagnostics.Debug.Print("term = " + term.ChildNodes[0].InnerText.ToString());
         if (term.ChildNodes.Count > 1)
         {
             System.Diagnostics.Debug.Print("value = " + term.ChildNodes[1].InnerText.ToString());
             if (term.ChildNodes[0].InnerText.ToString() == "SspId")
             {
                 // Get shared services ID from list
                 sharedServiceIds = term.ChildNodes[1].InnerText.ToString();
             }
             if (term.ChildNodes[0].InnerText.ToString() == "TermSetId")
             {
                 // Get Term Set ID from list
                 termSetId = term.ChildNodes[1].InnerText.ToString();
             }
         }
  
     }
     
  
     int lcid = System.Globalization.CultureInfo.CurrentUICulture.LCID;
     string serverTermSetTimeStampXml = "";


     string termStoreIds = "<termStoreIds><termStoreId>" + sharedServiceIds + "</termStoreId></termStoreIds>";
     string termSetIds = "<termSetIds><termSetId>" + termSetId + "</termSetId></termSetIds>";
     //Always set timestamp to this
     string oldtimestamp = "<timeStamps><timeStamp>633992461437070000</timeStamp></timeStamps>";
     //Always set version to 1
     string clientVersion = "<versions><version>1</version></versions>";
     string timeStamp = "";

     // Taxonomy web service!!
     Taxonomys.Taxonomywebservice taxonomy = new Taxonomys.Taxonomywebservice();
     taxonomy.Credentials = System.Net.CredentialCache.DefaultCredentials;
     string resultXML = taxonomy.GetTermSets(termStoreIds, termSetIds, lcid, oldtimestamp, clientVersion, out timeStamp);



     //Loop through the XML
     string termValue = "MHA";
     string termGUID = "";
     string parentID = "";
     XmlDocument termSetXML = new XmlDocument();
     termSetXML.LoadXml(resultXML);
     XmlNodeList terms = termSetXML.GetElementsByTagName("T");
     foreach (XmlNode term in terms)
     {
         string termName = term.FirstChild.FirstChild.Attributes["a32"].Value;
         termGUID = term.Attributes["a9"].Value;
         try
         {
             parentID = term.Attributes["a25"].Value;
         }
         catch (Exception)
         {
             parentID = "";
     
         }
  
         System.Diagnostics.Debug.Print("termName = " + termName + ", termGUID = " + termGUID + ", parentID = " + parentID);   
     }                          
     
 }
}


If you would like to know how to analyse the attribute codes returned in the XML, then take a look at this blog entry: http://blogs.solidq.com/sharepoint/Post.aspx?ID=37&title=SharePoint+2010%2C+Managed+Metadata%2C+TaxonomyClientService+in+depth

Thursday, 10 March 2011

VBA code to check in a document to Sharepoint and set meta data

Carissima Gold 9 ct Yellow Gold Two-Row Diamond Cut Curb Bracelet of 21 cm 8.5-inch on www.yngoo.co.uk
Carissima Gold 9 ct Yellow Gold Two-Row Diamond Cut Curb Bracelet of 21 cm/8.5-inch
 

The following VBA code will upload a file to Sharepoint 2007 using a web service, and then sest some meta data (the title) for that file in the document library.
Remember, you need to have installed the VBA Microsoft Office 2003 Web Services Toolkit and created a reference to the web service in your project before this will work. See these previous posts for more information on calling Sharepoint web services from VBA:







' Change these values to your own
sSourceFile = "C:\mtest.doc"  ' File to upload
sTargetFile = "HTTP://my sharepoint site/Shared%20Documents/mytest.doc" ' Target site, document library and file name


' First set up DOM document containing fields
Dim xmlDoc As New MSXML2.DOMDocument30
xmlDoc.async = False

xmlText = "<root>" & _
"<Batch OnError='Continue' ListVersion='" & iVersionNumber & "' PreCalc='TRUE' xmlns=''>" & _
"<Method ID='1' Cmd='Update'>" & _
"<Field Name='ID' />" & _
"<Field Name='FileRef'>" & sTargetFile & "</Field>" & _
"<Field Name='Title'>Uploaded from VBA</Field>" & _
"</Method>" & _
"</Batch>" & _
"</root>"


xmlDoc.LoadXml (xmlText)
Debug.Print xmlText

' This bit is just for testing
If xmlDoc.parseError.errorCode <> 0 Then

Set myErr = xmlDoc.parseError
MsgBox (myErr.reason)
GoTo fnUpload_Error
Else
MsgBox xmlDoc.XML
End If

' Set up IXMLDOMNodeList
Dim myXMLNodeList As MSXML2.IXMLDOMNodeList
Dim root As MSXML2.IXMLDOMElement


Set root = xmlDoc.documentElement
Set myXMLNodeList = root.ChildNodes

' Create an array of IXMLDOMNodeList
Dim ar_Fields(1) As IXMLDOMNodeList
Set ar_Fields(0) = myXMLNodeList
'Debug.Print "ar_Fields(0) = " & ar_Fields(0)


' Now set up an array of strings to hold the URL
Dim ar_URL(1) As String
ar_URL(0) = sTargetFile

Debug.Print "ar_URL(0) = " & ar_URL(0)
' Set up the results object
Dim myresults() As struct_CopyResult

' Set up the byte array and read the source file into it
Dim ar_Stream() As Byte
ar_Stream = ReadFile(sSourceFile)


' NOW CALL WEB SERVICE
' The follwoing comes from the "Microsoft Office 2003 Web Services Toolkit":
'"ar_DestinationUrls" is an array with elements defined as String
'"ar_Fields" is an array with elements defined as IXMLDOMNodeList
'"ar_Stream" is an array with elements defined as Byte
'"ar_Results" is an array with elements defined as struct_CopyResult
'See Complex Types: Arrays in Microsoft Office 2003 Web Services Toolkit Help
'for details on implementing arrays.
documentId = copyws.wsm_CopyIntoItems(sDocumentPath, ar_URL, ar_Fields, ar_Stream, myresults)
Debug.Print "DocumentID = " & documentId

Dim updateReturn As IXMLDOMNodeList

Set updateReturn = listws.wsm_UpdateListItems(sListID, myXMLNodeList)

Dim xmlReturnDoc As New MSXML2.DOMDocument30
If (updateReturn.Length > 0) Then    
xmlReturnDoc.LoadXml (updateReturn.Item(0).XML)

Dim errorText As String
errorText = xmlReturnDoc.Text
If (errorText <> "0x00000000") Then
MsgBox ("Error: Cannot upload load file to Sharepoint." & vbCrLf & _
"     : " & errorText & vbCrLf & Err.Description & vbCrLf )

End If
End If

' Uncomment for debug information.
'MsgBox ("Return XML = " & xmlReturnDoc.XML)


I use the following VBA to read the target file in as a byte array:

Private Function ReadFile(ByVal strFileName As String, Optional ByVal lngStartPos As Long = 1, Optional ByVal lngFileSize As Long = -1) As Byte()
    Dim FilNum As Integer
    FilNum = FreeFile
    Open strFileName For Binary As #FilNum
    If lngFileSize = -1 Then
        ReDim ReadFile(LOF(FilNum) - lngStartPos)  
    Else
        ReDim ReadFile(lngFileSize - 1)
    End If
    Get #FilNum, lngStartPos, ReadFile
    Close #FilNum
End Function

Wednesday, 26 January 2011

Sharepoint 2007 Batch Update example using web services and CAML

Here is an example of running a batch update process on Sharepoint 2007 using web services. It was written in C# in Visual Studio 2008.

In this example, I take the value of a text field "Date_x0020_of_x0020_Birth", turn this into a DateTime value, and then use this to update the the value of a DateTime field "Date_x0020_of_x0020_Birth0". Obviously this section of the code will not be particularly generalisable, however I have left in all the code in order to show you the principles involved, and hopefully you will find it useful in building your own batch routines.

In order to get it running, you must create a reference to the Sharepoint Lists web service and call it "ListsService". The SDK shows you how to do this, and is available here: http://www.microsoft.com/downloads/en/details.aspx?FamilyId=6D94E307-67D9-41AC-B2D6-0074D6286FA9&displaylang=en

The GetListItems() method of the Lists web service is used to query the data, and the UpdateListItems() method is used to update items.

I used an XPathNavigator object to filter only rows that contain data. You could just loop through the nodes if you prefer.

CAML is used to query the Sharepoint list and look for items where the "Date_x0020_of_x0020_Birth" is null. Tailor this to your own needs.
For CAML reference, look at the MSDN article here: http://msdn.microsoft.com/en-us/library/lists.lists.getlistitems%28v=office.12%29.aspx

The CAML update command uses owshiddenversion, and so is useful in situations where versioning is enabled. Lists without versioning do not need this field.

If the UpdateListItems command returns an error, in my experience this is usually caused by using an incorrect internal name for a field.  You may need to add the URI prefix "urn:schemas-microsoft-com:office:office#" to the field name (e.g. change line 42 to something like:  "<Field Name=\"urn:schemas-microsoft-com:office:office#{4}\">{5}</Field>").

Full code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Net;

namespace BatchUpdateDOB
{
    class Program
    {
        static string LogFile = "D:\\BatchUpdateLog.txt";  // Log file location

        static void Main(string[] args)
        {
            try
            {
                StatusMessage("Writing log file to: " + LogFile);
           
                // Set up the variables to be used.
                StringBuilder methodBuilder = new StringBuilder();
                string batch = string.Empty;
                DateTime currentDate = DateTime.Now;
                string formattedDate = SPUtility.CreateISO8601DateTimeFromSystemDateTime(currentDate);
                string fieldToReference = "Date_x0020_of_x0020_Birth";
                string fieldToUpdate = "Date_x0020_of_x0020_Birth0";

                string batchFormat = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                  "<ows:Batch OnError=\"Return\">{0}</ows:Batch>";

                
                // CAML update command for web service
                string methodFormat = "<Method ID=\"1\" Cmd=\"Update\">" +
                 "<Field Name=\"ID\">{1}</Field>" +
                 "<Field Name=\"FileRef\">{2}</Field>" +
                 "<Field Name=\"owshiddenversion\">{3}</Field>" +
                 "<Field Name=\"{4}\">{5}</Field>" +
                 "</Method>";

                //UPDATE THESE TWO LINES TO SITE SPECIFIC VALUES
                string siteName = "http://<your site root uri here>";
                string listName = "<your list name here>";

                //string viewName = "";  // No need for viewname
                int rowLimit = 10;       //The number of items returned per batch (not used)

                StatusMessage("Processing " + siteName + ", List: " + listName + "...");

                //Web service setup
                //ListService should be the name of your web service reference
                ListService.Lists listService = new ListService.Lists();
                listService.Credentials = System.Net.CredentialCache.DefaultCredentials;
                listService.Url = siteName + "/_vti_bin/Lists.asmx";
                

                //Build CAML query to search for records
                XmlDocument camlDocument = new XmlDocument();
                XmlNode queryNode = camlDocument.CreateElement("Query");
                //queryNode.InnerXml =  "<Where><IsNull><FieldRef Name='Date_x0020_of_x0020_Birth'/></IsNull></Where>";
                //queryNode.InnerXml = "<OrderBy><FieldRef Name='Country' /></OrderBy>";

                XmlNode viewFieldsNode = camlDocument.CreateElement("ViewFields");
                viewFieldsNode.InnerXml = "<FieldRef Name='Title' />" +
                  "<FieldRef Name='owshiddenversion' />" +
                  "<FieldRef Name='" + fieldToReference + "' />" +
                  "<FieldRef Name='" + fieldToUpdate + "' />" +
                  "<FieldRef Name='FileRef' />";

                XmlNode queryOptionsNode = camlDocument.CreateElement("QueryOptions");
                queryOptionsNode.InnerXml = "<ViewAttributes Scope=\"Recursive\" /><IncludeMandatoryColumns>True</IncludeMandatoryColumns>";

                // Get the GUID of the list
                XmlNode ndListView = listService.GetListAndView(listName, "");
                string listGuid = ndListView.ChildNodes[0].Attributes["Name"].Value;
                string listVersion = ndListView.ChildNodes[0].Attributes["Version"].Value;

                // Run the CAML query
                XmlNode resultNode = listService.GetListItems(listGuid, null, queryNode, viewFieldsNode, rowLimit.ToString(), queryOptionsNode, null);
                // If you want to put a rowlimit on, then use this command:
                //XmlNode resultNode = listService.GetListItems(listGUID, null, queryNode, viewFieldsNode, rowLimit.ToString(), queryOptionsNode, null);

                long totalRecords = 0;
               
                //string itemDOB;
                string dob;
                DateTime dtDOB;
                string owsHiddenVersion;  
                string fullFileRef;
                string owsFileRef;
               

                // Create xpath navigator   
                XPathNavigator navigator = resultNode.CreateNavigator();
                // Set up namespace manager for XPath   
                XmlNamespaceManager ns = new XmlNamespaceManager(navigator.NameTable);
                ns.AddNamespace("rs", "urn:schemas-microsoft-com:rowset");
                ns.AddNamespace("z", "#RowSetSchema");

                // Select only the z:row nodes
                XmlNodeList nodes = resultNode.SelectNodes("//*[name()='z:row'] ", ns);

                // Iterate through the returned nodes
                foreach (XmlNode node in nodes)
                {
                    string itemID = node.Attributes["ows_ID"].InnerText;
                    try
                    {
                        dob = node.Attributes["ows_" + fieldToReference].InnerText;
                        owsHiddenVersion = node.Attributes["ows_" + "owshiddenversion"].InnerText;
                        owsFileRef = node.Attributes["ows_" + "FileRef"].InnerText;
                        // For some reason my data is returned in the format: 2#/listname/path, 
                        // where 2 seems to be an id or version number, and listname/path points to the file in relation to the root site.
                        // The following strips that out and gets the full path (required for the CAML update)
                        fullFileRef = siteName + "/" + owsFileRef.Remove(0, owsFileRef.IndexOf("#") + 1);
                    }
                    catch(Exception ex)
                    {
                        // Ignore this row
                        //StatusMessage("ERROR: Cannot process: " + node.InnerXml.ToString() + ": " + ex.Message); // If you want to see why, uncomment
                        continue;
                    }

                    if (String.IsNullOrEmpty(dob)) continue;  // Ignore list items with no value

                    dtDOB = Convert.ToDateTime("01/01/1900"); // Set default value for dates before 1900

                    // Convert date text to DateTime
                    try
                    {
                        dtDOB = Convert.ToDateTime(dob);
                    }
                    catch (Exception ex)
                    {
                        StatusMessage("Cannot process: " + siteName + "/" + fullFileRef + " DOB: " + dob + ".  Setting to 01/01/1900. " + ex.Message);
                    }

                    MessageToFile("Processing: " + fullFileRef + ", DOB = " + dob);

                    methodBuilder.AppendFormat(methodFormat, listGuid, itemID, fullFileRef, owsHiddenVersion, fieldToUpdate, SPUtility.CreateISO8601DateTimeFromSystemDateTime(dtDOB));

                    // Put the pieces together.
                    batch = string.Format(batchFormat, methodBuilder.ToString());
                    //MessageToFile(batch);

                    //Build CAML command to update using Web service
                    XmlDocument xmlDoc = new XmlDocument();
                    XmlElement elBatch = xmlDoc.CreateElement("Batch");
                    elBatch.SetAttribute("OnError", "Continue");
                    elBatch.SetAttribute("ListVersion", listVersion.ToString());
                    elBatch.SetAttribute("ViewName", "");
                    elBatch.InnerXml = methodBuilder.ToString();

                    // This line runs the CAML update command
                    XmlNode updatesResponse = listService.UpdateListItems(listGuid, elBatch);
                    if (updatesResponse.FirstChild.FirstChild.InnerText != "0x00000000") throw new Exception("Could not update properties.\n\n" + updatesResponse.InnerText + "\n" + updatesResponse.FirstChild.FirstChild.InnerText);
                    StatusMessage(fullFileRef + " processed...");

                    totalRecords++;
                }
                StatusMessage("Completed - " + totalRecords + " processed.");
            }
            catch (Exception exp)
            {
                StatusMessage("ERROR: " + exp.Message);
                
            }
            
        }

        
        /// <summary>
        /// Display a  message
        /// </summary>
        /// <param name="myMessage"></param>
        static void StatusMessage(string myMessage)
        {
            Console.WriteLine(myMessage.ToString()); 
            MessageToFile(myMessage);
        }

        /// <summary>
        /// Write message to the log file
        /// </summary>
        /// <param name="myMessage"></param>
        static void MessageToFile(string myMessage)
        {
            using (StreamWriter sw = new StreamWriter(LogFile, true))
            {
                DateTime now = DateTime.Now;
                sw.WriteLine(now.ToString() + ": " + myMessage.ToString());
            }
        }
    }
}

Wednesday, 21 April 2010

VBA code to iterate through the results of GetListCollection web service from Sharepoint 2007


In a previous post I discussed how to set meta-data in Sharepoint 2007 directly from VBA.  I mentioned that the Microsoft documentation for the Sharepoint web services does not appear to be correct, insofar as you cannot address a Sharepoint "list" in the web service using its name, but rather you are forced to use the GUID.

I have now had to write a function to retrieve the GUID for the list from teh Sharepoint site.  This proved to be much harder in practice than I had expected it to be, partly at least because of the problems in working in the VBA IDE with its poor debugging functionality, partly because VBA is a much more limited language than .Net and I have got used to using all the nice constructs such as generics that make life so much easier,  and partly because I still find MSXML very "brain" hard to work with.  I don't know why I find it so hard, it just always seems that everything ends up being far more complicated than I think it should be.  I find the .Net XML model much easier to deal with.

Anyway, after playing about with XSLT and XPATH in order to extract the ID and VersionNumber attributes from the wsm_GetListCollection web service (and failing miserable), I finally decided to just iterate through the nodes and attributes till I found what I wanted.  The code below is a simple function that you can call with three parameters:  the name of the list that you need the GUID for (sListName),  a string variable in which the GUID will be placed (sListID), and an integer that the list version number will be placed (iListVersion).

Note: You may also be interested in this post: VBA code to check in a document to Sharepoint and set meta data.


Function GetListCollection(ByVal sListName As String, ByRef sListID As String, ByRef iListVersion As Integer)
    ' Class created by toolkit to connect to the Web service
    Dim ws As New clsws_Lists
    ' The collection is returned as an XML node list
    Dim ListCollectionNodeList As MSXML2.IXMLDOMNodeList
    'Root node of the returned list
    Dim nod As MSXML2.IXMLDOMNode
    ' Output string for the XML transformation
    Dim strOutput As String
 
    On Error GoTo GetListCollection_OnError
 
    ' Retrieve the collection of lists
    Set ListCollectionNodeList = ws.wsm_GetListCollection
 
    ' Get the root node from the list
    Set nod = ListCollectionNodeList.Item(0)
 
 
    ' Iterate nodelist to find details for our named list
    Dim bGotNode
    bGotNode = False
 
 
    Dim procNode As MSXML2.IXMLDOMNode
    Dim TitleNodeList As MSXML2.IXMLDOMNodeList
    Dim IDNodeList As MSXML2.IXMLDOMNodeList
    Set TitleNodeList = nod.SelectSingleNode("//Title")
    Set IDNodeList = nod.SelectSingleNode("//ID")
 
    Dim ListNode As MSXML2.IXMLDOMNode
    Dim ListChildNodes As MSXML2.IXMLDOMNode
    Dim oAttr As MSXML2.IXMLDOMAttribute
 
    Dim retID As String
    Dim listVersion As Integer
    Dim ListName As String
 
    ' Iterate through list nodes returned (should only be one)
    For Each ListNode In ListCollectionNodeList
        'Iterate through the child nodes (each one represents a single Sharepoint list)
        For Each ListChildNodes In ListNode.ChildNodes
            'See if we have any attributes (we should have!)
            If ListChildNodes.Attributes.Length > 0 Then
                ' For each list, reset our variables
                retID = ""
                listVersion = 0
 
                ' Iterate through the attributes
                For Each oAttr In ListChildNodes.Attributes
                    Debug.Print "Name= " & oAttr.Name
                    Debug.Print "Value= " & oAttr.nodeTypedValue
 
                    'Look for the ID attribute
                    If oAttr.Name = "ID" Then
                        ' FOund it, so remember it in case this is our list
                        retID = oAttr.nodeTypedValue
                    End If
 
                    ' Look for the "Title" attribute
                    If oAttr.Name = "Title" Then
                        If oAttr.nodeTypedValue = sListName Then
                            ' Found our list, so remember it
                            ListName = oAttr.nodeTypedValue
                            bGotNode = True
                        End If
                    End If
 
                    ' Look for the "Version" attribute
                    If oAttr.Name = "Version" Then
                            ' Found our list, so remember it
                            listVersion = oAttr.nodeTypedValue
                    End If
 
                    ' Once we have found a title, ID and version, break
                    If retID <> "" And listVersion <> 0 And bGotNode = True Then
                       Exit For
                    End If
 
                Next oAttr
            End If
            ' Once we have found the data, break out of iteration
            If bGotNode = True Then
                Exit For
            End If
        Next
    Next
 
    If bGotNode = False Then
        Err.Raise 65000, "", "ERROR: Cannot find list named <" & sListName & "> in Sharepoint site.", "", ""
    End If
 
    sListName = ListName
    sListID = retID
    iListVersion = listVersion
 
    Exit Function
 
GetListCollection_OnError:
    MsgBox ("ERROR: Retrieving list collection from Sharepoint." + vbCrLf + _
           Err.Number + " - " + Err.Description)
End Function

Remember, you will have to have installed the Microsoft Web Services Toolkit before this will work, and add a web reference to your Sharepoint site to your VBA project. This has been tested with Office 2003 linking to Sharepoint 2007 and works :).

Wednesday, 10 March 2010

Set Sharepoint meta-data from VBA using updatelist web service

For the top ten best selling Rolex watches, visit Yngoo!
I've been working on a problem trying to integrate a legacy Microsoft Office 2003 VBA application with Sharepoint 2007. In theory, I should be able to use the SOAP toolkit provided my Microsoft to talk to the Sharepoint (WSS) web services, but in practice there was very little documentation of how to do this, and I could find no resources on the internet aside from the MSDN definitions of the web services, and only a little attached to the SOAP toolkit (available here: Microsoft Office 2003 Web Services Toolkit ).

The basic problem I was trying to solve was: how do I set meta-data held on a document in Sharepoint from Office 2003 using VBA?

After much head scratching I finally solved this by making some assumptions based on the MSDN reference documentation, then using the Fiddler tool until I had worked out what I was doing wrong.  Fiddler allows you to inspect the HTTP calls and responses that you are issuing/receiving.  This is very helpful when debugging web services, and well worth looking into if you are doing anything in this area.

Anyway, through Fiddler I could see the response from the web service, including the error message.  This meant that my initial best guess on how to structure the XML was at least nearly correct, because the web service was giving me a proper error message.  Eventually I had a properly structured call and everything worked!


Remember to install the SOAP toolkit before you try this.  You will then be able to add this to your VBA project and add a web reference to your project to the WSS Lists.asmx service as you would if your using .Net.  This is all explained in the MSDN link above but drop a comment if you would like me to go into more detail.

The actual VBA code used structure the call to the web service looks like this:

'Define a new list service web service class
Dim listws As New clsws_Lists

' Set up the batch command used to set the meta data. 
' Note that FileRef is set to URL of the file you want to change
'ListVersion is from my test site, you may not need that
' First set up DOM document containing fields
Dim xmlDoc As New MSXML2.DOMDocument30
xmlDoc.async = False
Dim xmlText As String
xmlText = "<root>" + _
  "<Batch OnError='Continue' ListVersion='59' PreCalc='TRUE' xmlns=''>" + _
      "<Method ID='1' Cmd='Update'>" + _
        "<Field Name='ID' />" + _
        "<Field Name='FileRef'>http://myservername:1080/sites/mysite/shared documents/TestWebService.doc</Field>" + _
        "<Field Name='Title'>Uploaded from VBA</Field>" + _
        "<Field Name='Surname'>" + sLastName + "</Field>" + _
        "<Field Name='Forename'>" + sFirstName + "</Field>" + _
        "<Field Name='Date_x0020_of_x0020_Birth'>" + sDOB + "</Field>" + _
      "</Method>" + _
    "</Batch>" + _
  "</root>"
xmlDoc.LoadXml (xmlText)

' This is a bit of debug that checks the XML is well formed and show you it if it is. 
If xmlDoc.parseError.ErrorCode <> 0 Then
    Dim myErr
    Set myErr = xmlDoc.parseError
    MsgBox (myErr.reason)
Else
    MsgBox xmlDoc.XML
End If

' Set up IXMLDOMNodeList object
Dim myXMLNodeList As MSXML2.IXMLDOMNodeList
Dim root As MSXML2.IXMLDOMElement
' Now the the XML node list to the batch command we have just constructed above
Set root = xmlDoc.documentElement
Set myXMLNodeList = root.ChildNodes

' Now run the web service to update the meta-data.
' Note that the first parameter is the GUID of the list that holds the data you want to change.  I can't get this to take a list name - it only seems to wrok with a GUID.
Dim updateReturn As IXMLDOMNodeList
Set updateReturn = listws.wsm_UpdateListItems("{B768B024-8B98-4918-990A-ECE34691DBBC}", myXMLNodeList)

If you need to find out the GUID or ListVersion values for your list, use the "GetListCollection" web service and iterate through the XML it returns:

Dim listCollection As IXMLDOMNode
Set listCollection = listws.wsm_GetListCollection
' You can iterate this to find the GUID of our list
I can post further examples if anybody wants any.

*Edit* - I have written another blog post on how to find the GUID of lists in VBA here: http://the-simple-programmer.blogspot.com/2010/04/vba-code-to-iterate-through-results-of.html
This simple function will return the version number and the GUID of the Sharepoint list from its name.  

You may also be interested in teh following post: :  VBA code to check in a document to Sharepoint and set meta data.



Friday, 27 November 2009

Creating Sharepoint list fields on the fly with web services

I've recently been trying to create Sharepoint 2007 list fields on the fly using the Lists.UpdateLists() method described here:
http://msdn.microsoft.com/en-us/library/lists.lists.updatelist.aspx

Unfortunately, after modifying the sample code to my needs, a SOAP exception kept getting thrown: "NodeType 'Element' is not an valid type":

<errorstring xmlns="http://schemas.microsoft.com/sharepoint/soap/">'Element' is an invalid XmlNodeType. Line 1, position 10.</errorstring>

Using Visual Studio 2005 to debug,  I have been unable to work out what the problem was.  The CAML query built in the webservice to create the field must be incorrect, but the innerXML of the CAML query (I was only using the "updateFields" node) looked fine.  Clearly the problem was elsewhere in the call, but is not clear to me how the call is constructed once the parameters are passed to the web service.

SOAP does not seem to me to give a huge amount of debugging information when a call fails.  When I create my own web services, I can use VS to step through them and get lots of debug information, but I haven't been able to work out how to do this with the MS DLLs yet.

Anyway, I eventually solved this problem by going back to the example and creating another copy of the example Microsoft code with minimal changes in order to point the call at my webservice.  This proved to work OK, although it did create two sample fields (Field1 and Field2) that I cannot remove using the Sharepoint IE front end!.  I suspect this is becuase they are created as required fields.

By making minimal iterative changes, I finally managed to create the following c# method which successfully creates the fields on the fly, and even gives them an appropriate Sharepoint type.

(Please note this code looks better in IE than Firefox which is not wrapping the lines - click on "View Plain" to view or copy in Firefox)

public bool CreateNewField(ListInfo listInfo, string fieldName, object fieldValue)
{
    bool canCreateField = true;
    try
    {

                //ListsService.Lists listService = new ListsService.Lists();
                //listService.Credentials= System.Net.CredentialCache.DefaultCredentials;
                //NOTE: I have already instatiated my connection (listService) and the GUID of the list (listInfo.listName)

                XmlNode ndList = listService.GetList(listInfo.listName);
                XmlNode ndVersion = ndList.Attributes["Version"];

                XmlDocument xmlDoc = new System.Xml.XmlDocument();

               

                XmlNode ndNewFields = xmlDoc.CreateNode(XmlNodeType.Element,
                    "Fields", "");
                XmlNode ndUpdateFields = xmlDoc.CreateNode(XmlNodeType.Element,
                    "Fields", "");

               
                
                // Field types
                if(fieldValue.GetType() == Type.GetType("System.String"))
                {
                    ndNewFields.InnerXml = @"<Method ID='1'>" +
                   "<Field Type='Text' DisplayName='" + fieldName + "' Required='FALSE' FromBaseType='FALSE' Description='Generated Field'/>" +
                   "</Method>";
                }
                else if (fieldValue.GetType() == Type.GetType("System.Int") || 
                         fieldValue.GetType() == Type.GetType("System.UInt16") ||
                    fieldValue.GetType() == Type.GetType("System.UInt32") ||
                    fieldValue.GetType() == Type.GetType("System.UInt64")
                    )
                {
                    ndNewFields.InnerXml = @"<Method ID='1'>" +
                  "<Field Type='Integer' DisplayName='" + fieldName + "' Required='FALSE' FromBaseType='FALSE' Description='Generated Field'/>" +
                  "</Method>";
                }
                else if (fieldValue.GetType() == Type.GetType("System.Double"))
                {
                    ndNewFields.InnerXml = @"<Method ID='1'>" +
                  "<Field Type='Number' DisplayName='" + fieldName + "' Required='FALSE' FromBaseType='FALSE' Description='Generated Field'/>" +
                  "</Method>";
                }
                else if (fieldValue.GetType() == Type.GetType("System.Date"))
                {
                    ndNewFields.InnerXml = @"<Method ID='1'>" +
                  "<Field Type='DateTime' DisplayName='" + fieldName + "' Required='FALSE' FromBaseType='FALSE' Description='Generated Field'/>" +
                  "</Method>";
                }
                else if (fieldValue.GetType() == Type.GetType("System.Array"))
                {
                    ndNewFields.InnerXml = @"<Method ID='1'>" +
                  "<Field Type='Text' DisplayName='" + fieldName + "' Required='FALSE' FromBaseType='FALSE' Description='Generated Field'/>" +
                  "</Method>";
                }
                else
                {   // Data type not handled - skip field
                    //Logger is my helper class - Remove from your code
                    Logger.LogMessageToFile("WARNING: Undefined field <" + fieldName + "> has an unhandled data type of: " + fieldValue.GetType().ToString() + "\r\n" +
                        "This field cannot be created");
                    canMigrateField = false;

                }
                Type myType = fieldValue.GetType();
                
                if (canCreateField)
                {
                    //NOTE: You need to use the GUID in the listname field.  I have this pre-populated in the listInfo.ListName class
                    XmlNode ndReturn =
                     listService.UpdateList(listInfo.listName,
                     null, ndNewFields, null, null,
                     ndVersion.Value);


             Logger.LogMessageToFile("NEW FIELD <" + fieldName + "> Created successfully.\r\n");
        }

    }

    catch (Exception ex)
    {
        Logger.LogMessageToFile("Message:\n" + ex.Message + "\nStackTrace:\n" + 
            ex.StackTrace);
        Logger.PopupMessage();
    }

    if (canCreateField) return true;
    else return false;
}