Showing posts with label c#. Show all posts
Showing posts with label c#. Show all posts

Thursday, 18 September 2014

Programatically overriding SharePoint record locks using Records.BypassLocks() in c#

Records are good - they are immutable and cannot be changed by users.

Record Centres are also good - they provide a repository for records that users can submit their documents to, to be transformed into records.

Event receiver rules that automatically commit as records any document submitted to a list are also good, because as soon as content is placed in that list it becomes a records, and so immutable.

However...

What if something goes wrong?  Perhaps a users submits a document that should not be locked as a record ans should be removed, or perhaps the wrong meta data is attached to the record?

In these cases an admin has to temporarily suspend the records management features of SharePoint in order to rectify the fault.  This is a big deal since SharePoint admins have a different role to server admins, and so normally would not have the authority (or indeed the know-how) to do this, whereas the server admins would not be able to understand the records and content.  In my experience the only people who know both how the system works, and what it is supposed to do, are the developers!  And we do NOT want to do admin, because among other things they somehow get paid more than we do...

Furthermore, if you are automatically declaring documents as records when they enter the list, you will not be able to "undeclare" the record from the SharePoint front end without first disabling this feature - quite a risk if you end up having to do this a lot.

This is why I like to create my own tools to fill the gaps, and in this case we programmers can turn to a little known feature of the Object Model to help us:   Records.BypassLocks().

This neat little method, part of the  Microsoft.Office.RecordsManagement.RecordsRepository namespace, allows use to define a delegate function that be be run even on locked down records.  Wrap it in a security delegate and you have the makings of a class that can be used to provide users (preferably power users and admins) with tools to manage their records centre WITHOUT suspending record handling functions.

Remember - this allows you to override records handling, so only use this if you really need to.  However if you have found this blog and read this far, you probably really need to!

Here is a basic method that shows you how to use Records.BypassLocks() in a simple record cancelling scenario.  In this example I pass in URL to the offending record, open a security delegate (in order to ensure that the process has permission to perform this task), and then use BypassLocks to edit the meta-data on the document to change the name and title and set the Status to "CANCELLED".

Please not that I have present this a method inside a class with no constructor - I will leave that up to you (simple copy the methods into whatever class you are working in if this confuses you).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Utilities;
using Microsoft.Office.RecordsManagement.RecordsRepository;  //You are going to need to include the dll in teh references
using System.Web;

namespace BusinessLayer
{

   public void Cancelrecord(string documentURL)
   {
        // Pass in the document URL in a format such as:
        //string documentURL = "http://test_reocrds/records/wrong_file.pdf";  

        //Run this in a security delegate

        SPSecurity.RunWithElevatedPrivileges(delegate() {

                using (SPSite site = new SPSite(SPContext.Current.Web.Url))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        web.AllowUnsafeUpdates = true;
                        try
                        {

                            SPListItem target = web.GetListItem(documentURL);

                            Records.BypassLocks(target, delegate(SPListItem item)
                            {         //Perform any action on the item. 
                                       CancelItem(item);
                            });

                        }
                        catch (Exception ex)  // it all throws up to here
                        {
                             throw ex;
                        }
                        finally
                        {

                            web.AllowUnsafeUpdates = false;

                        }
                    }
                }
            });
       }

        //Set item name to start with CANCELLED and set status to CANCELLED

        public void CancelItem(SPListItem item)
        {
            try
            {        

                // Update the name of the file

                item.File.CheckOut();
                item.TrySetValue("Name", String.Format("CANCELLED {0}", item.Name));
                item.TrySetValue("Title", String.Format("CANCELLED {0}", item.Title));
                item.TrySetValue("Status", "CANCELLED");  //

                item.Update();

                //This records library is set to use check in/out and versioning so it records any updates such as this

                item.File.CheckIn("CANCELLED", SPCheckinType.MinorCheckIn);

            }
            catch (Exception ex)
            {
                // Permissions failed.  Undo checkout
                item.File.UndoCheckOut();
                throw ex;
            }
        }
    }

}


 In my example above I use an Extensions class to add useful methods to SPListItem class to get or set values.  Here is the class:
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
namespace BusinessLayer

{

    public static class Extensions

    {

        public static T TryGetValue(this SPListItem listItem, string fieldInternalName)
        {
            if (!String.IsNullOrEmpty(fieldInternalName) &&
                listItem != null &&
                listItem.Fields.ContainsField(fieldInternalName))
            {
                object untypedValue = listItem[fieldInternalName];
                if (untypedValue != null)
                {
                    var value = (T)untypedValue;

                    return value;
                }
            }

            return default(T);
        }

        public static bool TrySetValue(this SPListItem listItem, string fieldInternalName, T value)
        {
            try
            {
                if (!String.IsNullOrEmpty(fieldInternalName) &&
                    listItem != null &&
                    listItem.Fields.ContainsField(fieldInternalName))
                {
                    listItem[fieldInternalName] = value;
                }

                return true;
            }
            catch
            {
                throw;
            }
        }
    }
}








Tuesday, 17 September 2013

Online tools to convert code between VB and C#

Using .Net and the CLR means we have sometimes have to use both VB and C#. If you are anything like me you will forget the more subtle syntactical differences between the two more often than not, a situation I found myself in earlier today when trying to work out how to rewrite an old VB function in C#.

Rather than doing this myself (I very rarely use VB anymore) I decided to try to use some of the resources on the internet.  The following sites will all allow you to convert freely between the two languages by pasting code into a browser control:

CodeTranslator: Code Translation From VB.NET <-> C# - CarlosAG - my favourite - this one managed to convert things the others couldn't.

Convert VB.NET to C# - developerFusion

Convert VB to C# or C# to VB provided by Telerik

Hope this helps!

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

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());
            }
        }
    }
}

Tuesday, 11 January 2011

Binding a CheckBoxList object to an ordered Sharepoint list of reference values in a web part (c#)

It's common when creating database applications to use reference tables to provide lookup values.  These tables ofter contain a flag that is used to mark a record as no longer available, and an order column that provides a method of putting the values in an order of importantance, perhaps when using them in a drop down list or list of checkboxes.  Sharepoint is NOT a database, however there is no reason why good practice such as this cannot be applied to Sharepoint's list model.

In the following example I will show you how to set up a reference list and then use the Sharepoint object model to programmatically access this list and bind it to an control (in this case a Sharepoint CheckBoxList control).  Once you understand this, you can use the same method to connect to any reference list and bind it to other objects and controls, such as drop down lists. This example has been developed for Sharepoint 2007, but should also work for Sharepoint 2010.








Step 1:  Set up a reference list
Create a new list on your Sharepoint site with teh following structure: 
  • ID - integer (this is a default field) - this will be used as the unique key (lookup value) used to link this reference filed to other lists
  • Title - string (this is a default field) - this is used to hold the description of the reference field, and will be the value shown in lookups
  • SortOrder - integer, default 1000 - this can be used to sort the list values in lookups and programmatically using CAML
  • Enabled - Yes/No field, default Yes - setting to NO will enable you to filter out these values in views and in programmatic lookups in CAML
You can now enter you reference values into the list, for example here is a list of types of medical record:

Step 2:  Add method to webpart that use CAML to read and order the list
Now for the code that reads the Sharepoint list in a structured manner so we can use it in web parts.  First, add the following method to your web part:
/// <summary>
/// Returns a list collection, ordered using a field in the list called "SortOrder"
/// </summary>
/// <param name="listName">The literal string name of the list to query</param>
/// <returns>SPListItemCollection</returns>
private SPListItemCollection ReadOrderedList(string listName)
{
    try
    {
        using (SPWeb configWeb = SPContext.Current.Site.OpenWeb())
        {
            //Get the list
            SPList lookupList = configWeb.Lists[listName];
            // Use CAML to order the list
            SPQuery query = new SPQuery();
            // This CAML where clause searches for only items where the Enabled field is set to "Yes"
            query.Query = "<Where><Eq><FieldRef Name=\"Enabled\" /><Value Type=\"Integer\">1</Value></Eq></Where> ";
            // This CAML order by claus uses an integer field of "SortOrder"
            query.Query += "<OrderBy><FieldRef Name=\"SortOrder\" /></OrderBy>";
            SPListItemCollection lookupItems = lookupList.GetItems(query);
            return lookupItems;
        }
    }
    catch (Exception ex)
    {
        ErrorTrap("ReadOrderedList: " + ex.Message + Environment.NewLine + ex.StackTrace.ToString());
        throw;
    }
} 

This method will return the content of the list in a SPListItemCollection object. CAML is use to order the list by the "SortOrder" column, and to ignore values that do not have the boolean field "Enabled" set to true.

Step 3:  Bind the list to a control
At the class level of your webpart, create the control that you wish to bind to (in this case a CheckBoxList):

CheckBoxList myCheckList = new CheckBoxList();

To bind this control to your Sharpoint list, put this code in the CreateChildControls() method. Simply replace "My List" with the real name of your list:
// Set up the Record Section checkboxes list
    SPListItemCollection dt = ReadOrderedList("My List");
    myCheckList.DataSource = dt;
    myCheckList.DataTextField = "Title";
    myCheckList.DataValueField = "ID";
    myCheckList.DataBind();

That's it! You now have a Sharepoint object bound to your Sharepoint list of reference values.  When rendered, the output will look something like this:


Yyou can use the following method to render the CheckBoxList to the web part.  This example puts the CheckBoxList in a DIV that fixes the maximum size of the list with a scrollbar.
/// <summary>
/// render a list of checkboxes  to the page
/// </summary>
void RenderCheckBoxes()
{
    try
    {
        // Set up list of checkboxes
        myCheckList.EnableViewState = true;
        myCheckList.CellSpacing = 0;
        myCheckList.CellPadding = 0;

        for (int i = 0; i < myCheckList.Items.Count; i++)
        {
            ListItem li = new ListItem(myCheckList.Items[i].Text, myCheckList.Items[i].Value);
           
            // Set all boxes to selected if not a postback
            if (!Page.IsPostBack)
            {
                myCheckList.Items[i].Selected = true;
            }

        }
        // Give the checklist an ID (can use this in javascript etc)
        myCheckList.ID = "RecordSections";

        //Add controls to web part
        Controls.Add(new LiteralControl(@"<TABLE border='0' bordercolor='red' cellpadding='0' cellspacing='0' valign='top'>"));
        Controls.Add(new LiteralControl("<TR>"));
        Controls.Add(new LiteralControl("<TD>"));
        Controls.Add(new LiteralControl(@"<div class=""ms-frombody""><b>Record Sections</b></div>"));
        Controls.Add(new LiteralControl("</TD>"));
        Controls.Add(new LiteralControl("</TR>"));
        Controls.Add(new LiteralControl("<TR>"));

        Controls.Add(new LiteralControl("<TD>\r\n"));
        Controls.Add(new LiteralControl(@"<input type=""hidden"" id=""ScrollPos"" runat=""server""/>"));

        Controls.Add(new LiteralControl(@"<div id='RecordSectionScroll' style=""OVERFLOW: auto; WIDTH: 247px;  TOP: 0px; HEIGHT: 300px"" onscroll=""javascript:document.getElementById('ScrollPos').value = this.scrollTop""> "));

        Controls.Add(myCheckList);
        Controls.Add(new LiteralControl("</div></TD>"));
        Controls.Add(new LiteralControl("</TR>"));     
        Controls.Add(new LiteralControl("</TABLE>"));
    }
    catch (Exception ex)
    {
        ErrorTrap("RenderCheckBoxes" + ex.Message + Environment.NewLine + ex.StackTrace.ToString());
    }
}

Note that I am using literal controls to render the check boxes in a HTML table. This is pretty horrible and I don't recommend it, but it does work.

Here is the ErrorTrap method. Note that this is just a simple error handling function and is not required to render a checklist bound to a Sharepoint list, but since it appears in the examples I though I would show you what I do.
/// <summary>
/// Give an error message
/// </summary>
/// <param name="myMessage"></param>
void ErrorTrap(string myMessage)
{
    Controls.Add(new LiteralControl("<strong>ERROR: " + myMessage.ToString() + "<BR/></strong>"));
    ErrorToFile(myMessage);          
}

/// <summary>
/// Write errors to the log file
/// </summary>
/// <param name="myMessage"></param>
void ErrorToFile(string myMessage)
{
    using (StreamWriter sw = new StreamWriter("D:\\SearchErrorLog.txt"))
    {
        DateTime now = new DateTime();
        sw.Write(now.ToString() + ": " + myMessage.ToString());
    }
}

Tuesday, 16 November 2010

Render Content Type fields as a form in Sharepoint Web Part using c#

In this blog entry I will give you some example code that will render a simple form in a Sharepoint 2007 web part. The form fields are taken from a pre-defined Sharepoint Content Type. I struggled to find any examples of this on the net, despite it seeming to me to be a fairly ordinary thing to want to do (after all, if you go to all the trouble of defined your own Content Types to organise your data, surely you would want to use them in your own web parts?).

In order to develop this, I used C# / Visual Studio 2008 and Sharpoint 2007. This should also work in Visual Studio 2005, although you will have to install the web part manually - Visual Studio 2008 has far tighter links to Sharepoint, meaning debugging and deploying is much easier. It should also work in VS2010 / Sharepoint 2010.

The following method will return a SPContentType object, you only need to pass it a list name and content type name.  The SPContentType object can be used later to iterate through its fields in oder to render controls based on the content type's fields.


/// <summary>
/// Returns the content type object
/// </summary>
/// <param name="listName"></param>
/// <param name="contentTypeName"></param>
/// <returns></returns>
SPContentType ReadContentType(string listName, string contentTypeName)
{

    //get the web
    this._web = Microsoft.SharePoint.SPContext.Current.Web;

    SPList dirList = this._web.Lists[listName];
    SPContentType contentType = dirList.ContentTypes[contentTypeName];

    //You could simply iterate here like this if you wish, like this:
    //foreach (SPField field in _contentType.Fields)
    //{
    //    string dispName = field.Title;
    //    SPFieldType type = field.Type;
    //    object defaultValue = field.DefaultValue;

    //}

    return contentType;
}

The following code overrides the "CreateChildControls" Sharepoint method to create our own controls based on the content type.  In this example, only text fields and date-times are rendered, but hopefully it straight forward enough for you can see how you could extend this. Simply change: 
"Your_List_Name" and "Your_Content_Type_Name" 
to your actual values from your Sharepoint site.
I use the "LiteralControl" method to render raw HTML on the page to format the form, which feels really nasty but is efective.

protected override void CreateChildControls()
{
    //Controls.Clear();
    //base.CreateChildControls();

    // Read in the content type for the target list
    SPContentType contentType = ReadContentType("Your_List_Name", "Your_Content_Type_Name");
    
    Controls.Add(new LiteralControl("<TABLE>"));

    foreach (SPField field in contentType.Fields)
    {
        // Ignore hidden or readonly fields
        if (field.Hidden) continue;
        if (field.ReadOnlyField) continue;     

        if (field.Type == SPFieldType.Text)
        {
            string dispName = field.Title;
            string staticName = field.StaticName;
            SPFieldType type = field.Type;
            object defaultValue = field.DefaultValue;

            Controls.Add(new LiteralControl("<TR>"));
            Controls.Add(new LiteralControl("<TD>"));
            Label lbl = new Label();
            lbl.Text = dispName;

            Controls.Add(lbl);

            Controls.Add(new LiteralControl("</TD>"));

            // Textbox
            TextBox tb = new TextBox();
            // I give my controls a unique ID so I can reference them later
            tb.ID = "xx_ctl_xx" + dispName;
            
            if(defaultValue != null)
                tb.Text = defaultValue.ToString();

            Controls.Add(new LiteralControl("<TD>"));
            Controls.Add(tb);
            Controls.Add(new LiteralControl("</TD>"));
            Controls.Add(new LiteralControl("</TR>"));

        }
        else if (field.Type == SPFieldType.DateTime)
        {
            
            string dispName = field.Title;
            string staticName = field.StaticName;
            SPFieldType type = field.Type;
            object defaultValue = field.DefaultValue;

            Label lbl = new Label();
            lbl.Text = dispName;

            Controls.Add(new LiteralControl("<TR>"));
            Controls.Add(new LiteralControl("<TD>"));
            Controls.Add(lbl);
            Controls.Add(new LiteralControl("</TD>"));

            // Datetime picker
            SPDatePickerControl dtp = new SPDatePickerControl();
            dtp.ID = "xx_ctl_xx" + staticName;
           
            if (defaultValue != null)
                dtp.SelectedDate = defaultValue.ToString();

            Controls.Add(new LiteralControl("<TD>"));
            Controls.Add(dtp);
            
            Controls.Add(new LiteralControl("</TD>"));
            Controls.Add(new LiteralControl("</TR>"));
        }       
    }
    Controls.Add(new LiteralControl("</TABLE>"));

    //In my code, I use the controls to perform a custom search.  The following code plugs in
    //the event handler to do this, based on the values the user puts into the controls rendered above.
    //This blog post will not go into the detail of this so I have commented the following lines out.
    //cmdSearch = new Button();
    //cmdSearch.Text = "Start Search";
    //cmdSearch.Click += new EventHandler(cmdSearch_Click);
    //this.Controls.Add(cmdSearch);

    //lblQueryResult = new Label();                     
    //this.Controls.Add(lblQueryResult);
    
    base.CreateChildControls();
}

Once deployed, you can add this web part to your site in the normal way using the web front end.  Depending on the fields in your content type, it should render something like this (web part highlighted in pink):

Friday, 22 January 2010

Manage c# threads easily using an array of BackgroundWorker class objects


I recently built a piece of software that processed files on a server.  There are hundreds of thousands of files, so this took a long time to run through, so I decided to use threads in order to process multiple files at once.

I wanted to be able to manage these threads, and be able to control how many threads I used by a programmatic variable so I could use more or less threads depending upon the power of the machine the process would be run on, and I could try different numbers of threads and benchmark the results to find the optimum number of threads.

The solution I came up with uses an array of .Net 2.0 BackgroundWorker class objects, initialised at run time to the size of the integer variable "maxThreads".

The following code sets up the variables and initialises the array:

static int maxThreads = 20;  //Make bigger or smaller, it's up to you!
private BackgroundWorker[] threadArray = new BackgroundWorker[maxThreads];
static int _numberBackGroundThreads ;  //Just for fun

// Set up the BackgroundWorker object by 
// attaching event handlers. 
private void InitializeBackgoundWorkers()
{
    for (int f = 0; f < maxThreads; f++)
    {
        threadArray[f] = new BackgroundWorker();
        threadArray[f].DoWork +=
            new DoWorkEventHandler(backgroundWorkerFiles_DoWork);
        threadArray[f].RunWorkerCompleted +=
            new RunWorkerCompletedEventHandler(backgroundWorkerFiles_RunWorkerCompleted);
        threadArray[f].ProgressChanged +=
            new ProgressChangedEventHandler(backgroundWorkerFiles_ProgressChanged);
        threadArray[f].WorkerReportsProgress = true;
        threadArray[f].WorkerSupportsCancellation = true;

    }
}


Each BackgroundWorker class has three event handlers assigned to it:
  1. backgroundWorkerFiles_DoWork - This delegate method is used to run the process
  2. backgroundWorkerFiles_RunWorkerCompleted - this delegate method is called once the "DoWork" method has completed
  3. backgroundWorkerFiles_ProgressChanged - this delegate method is used to pass information back to the calling thread, for example to report progress to the GUI thread.
This delegate methods are discussed at length on the MSDN site so I wont go into them in detail here other than to show you this simple code outline:
private void backgroundWorkerFiles_DoWork(object sender, DoWorkEventArgs e)
{
    //Just for fun - increment the count of the number of threads we are currently using.  Can show this number in the GUI.
    _numberBackGroundThreads --;
    
    // Get argument from DoWorkEventArgs argument.  Can use any type here with cast
    int myProcessArguments = (int)e.Argument;

    // "ProcessFile" is the name of my method that does the main work.  Replace with your own method!  
    // Can return reulsts from this method, i.e. a status (OK, FAIL etc)
    e.Result = ProcessFile(myProcessArgument);
}

private void backgroundWorkerFiles_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // Use this method to report progress to GUI
}

private void backgroundWorkerFiles_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // First, handle the case where an exception was thrown.
    if (e.Error != null)
    {
        MessageBox.Show(e.Error.Message);
    }

    // For fun - print out the result of the ProcessFile() method.
    debug.print = e.Result.ToString();

    // Just for fun - decrement the count of threads
    _numberBackGroundThreads --;
}

OK now comes the fun part. The following code shows a loop thorugh a large number of items. Rather than run ProcessFile() for each item in the loop in turn, we instead choose an unused thread to run it in. This allows the loop to step onto the next item, which also is allocated an empty thread.
// Some process with many iterations
for(int f = 0; f < 100000; f++)
{

    //Use the thread array to process ech iteration
    //choose the first unused thread.
    bool fileProcessed = false;
    while (!fileProcessed)
    {
        for (int threadNum = 0; threadNum < maxThreads; threadNum++)
        {
            if (!threadArray[threadNum].IsBusy)
            {   // This thread is available
                Debug.Print("Starting thread: " + threadNum);
        
                //Call the "RunWorkerAsync()" method of the thread.  
                //This will call the delegate method "backgroundWorkerFiles_DoWork()" method defined above.  
                //The parameter passed (the loop counter "f") will be available through the delegate's argument "e" through the ".Argument" property.
                threadArray[threadNum].RunWorkerAsync(f);
                fileProcessed = true;
                break;
            }
        }
        //If all threads are being used, sleep awhile before checking again
        if (!fileProcessed)
        {
            Thread.Sleep(50);
        }
    }
}

Using this technique, it's eay to create 2, 10, or even 100 threads to process each loop iteration asychronously, which speeds up execution time enormously.  Simply change the value of "maxThreads" to whatever you need!
Seksy Watches on Yngoo!
Click here for the bestselling Seksy Watches and deals on Yngoo!

Tuesday, 15 December 2009

A c# timer stopwatch class to help analyse lengthy program execution

I have recently been developing a piece of software that uses the old Microsoft COM Interop technology to walk through a Sharepoint 2001 folder structure. I found that this was working pretty slowly, but by the time I came around to do anything about it the code had got fairly complicated and it wasn't clear where the bottleneck was occuring.

To help understand the problem I created this simple static timer class:
class Timer
    {
        static public DateTime timeStart;
        static public DateTime timeEnd;
        static public bool debugTimer = true;  //Switch to false to turn off timing messages
 
        public  Timer()
        {
        }
 
        static public void TimerStart(string msg)
        {
            if (! debugTimer) return;
            timeStart = DateTime.Now;
            Console.WriteLine("TIMER: " + msg + " BEGUN: " + timeStart.ToString(), true);
        }
 
        static public void TimerEnd()
        {
            if (!debugTimer) return;
            timeEnd = DateTime.Now;
            TimeSpan totalTime = new TimeSpan();
            totalTime = timeEnd.Subtract(timeStart);
 
            Console.WriteLine("TIMER END: " + timeEnd.ToString() + " Total time: " + totalTime.Milliseconds, true);
        }
    }

In order to use this in your code, simply copy the class and put the following lines around whatever process you wish to monitor:
Timer.TimerStart("Beginning timer");
    //...
    //do lenghtly process
    //...
    Timer.TimerEnd();

This will give a nice message, with the time elapsed in milliseconds.

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;
}