How to Create Offline Surveys in SharePoint 2010

Using surveys out of the box in SharePoint 2010 give site collection administrators an easy way to gather information from end users, for reporting purposes. Unfortunately, if you need any special features for your surveys, the stock survey functionality quickly falls short. Custom development would then be needed to extend or enhance the functionality needed for surveys.

One such scenario requiring custom development is how to extract survey questions for offline use, along with importing survey responses from a Windows forms-based application back into SharePoint. This blog will detail the steps needed to provide this functionality. I am using SharePoint 2010, along with Visual Studio 2010 Professional Edition.

Extracting Survey Information

The first step in providing surveys offline is to extract questions from a survey. This can be done using event receivers in SharePoint 2010. Ideally you would create an event receiver that would fire when an item in a list is added, updated, or deleted, so you can capture any changes made to the survey questions. I am checking theBaseTypeof the list being added, as the event receiver will fire for other list types:


public override void FieldAdded(SPListEventProperties properties)
{
try
{
base.FieldAdded(properties);
// Only process XML Definition file for surveys
if (properties.List.BaseType == SPBaseType.Survey)
ProcessSurveyQuestions(properties.List, properties.Web.Site.RootWeb, properties.Web);
}
catch (Exception ex)
{
// Handle exception
}
}

Once you've determined that the list is a survey, getting all the information from that survey is just a matter of iterating through the list. There is a field property called SchemaXML that holds all the information for each question in the survey (in XML format), so it is a simple matter to create an XML document that represents all the survey questions contained in a survey:

int x, y;

int fCount = localSurveyList.Fields.Count;

string strFinalXML = "<Survey>";

strFinalXML += "<Title>" + localSurveyList.Title + "</Title>";

strFinalXML += "<URL>" + localSurveyList.DefaultViewUrl + "</URL>";

strFinalXML += "<SurveyURL>" + localSurveyList.DefaultViewUrl + "</SurveyURL><SurveyQuestions>";

// All questions and info are contained in list; just need to iterate through it

for (x = 0; x < fCount; x++)

{

// Do not process if fields are "Order" or "MetaInfo"

if ((localSurveyList.Fields[x].StaticName != "Order") &&

(localSurveyList.Fields[x].StaticName != "MetaInfo"))

{

// Fields need to be one of the SPFieldTypes specified in order to process

if (!localSurveyList.Fields[x].ReadOnlyField &&

(localSurveyList.Fields[x].Type == SPFieldType.Text ||

localSurveyList.Fields[x].Type == SPFieldType.Note ||

localSurveyList.Fields[x].Type == SPFieldType.Choice ||

localSurveyList.Fields[x].Type == SPFieldType.MultiChoice ||

localSurveyList.Fields[x].Type == SPFieldType.Boolean ||

localSurveyList.Fields[x].Type == SPFieldType.Currency ||

localSurveyList.Fields[x].Type == SPFieldType.DateTime ||

localSurveyList.Fields[x].Type == SPFieldType.GridChoice ||

localSurveyList.Fields[x].Type == SPFieldType.Lookup ||

localSurveyList.Fields[x].Type == SPFieldType.Number))

{

// Use SchemaXml to determine characteristics of the question

XmlDocument xmlDoc = new XmlDocument();

xmlDoc.LoadXml(localSurveyList.Fields[x].SchemaXml);

switch (localSurveyList.Fields[x].Type)

{

case SPFieldType.Text:

// question with a single line of text answer

strFinalXML += localSurveyList.Fields[x].SchemaXml;

break;

case SPFieldType.Lookup:

// Cannot use XMLSchema, so individual attributes are created

// and lookup list is retrieved manually

strFinalXML += "<Field Type=\"Lookup\" ";

strFinalXML += "DisplayName=\"" + xmlDoc.DocumentElement.Attributes["DisplayName"].Value + "\" ";

strFinalXML += "Required=\"" + xmlDoc.DocumentElement.Attributes["Required"].Value + "\" ";

strFinalXML += "EnforceUniqueValues=\"" + xmlDoc.DocumentElement.Attributes["EnforceUniqueValues"].Value + "\" ";

strFinalXML += "List=\"" + xmlDoc.DocumentElement.Attributes["List"].Value + "\" ";

strFinalXML += "ShowField=\"" + xmlDoc.DocumentElement.Attributes["ShowField"].Value + "\" ";

strFinalXML += "StaticName=\"" + xmlDoc.DocumentElement.Attributes["StaticName"].Value + "\" ";

strFinalXML += "Name=\"" + xmlDoc.DocumentElement.Attributes["Name"].Value + "\">";

// Load appropriate lookup list, using GUID

Guid listGUID = new Guid(xmlDoc.DocumentElement.Attributes["List"].Value);

SPList localList = localSurveySite.Lists[listGUID];

int fListCount = localList.ItemCount;

strFinalXML += "<CHOICES>";

for (y = 0; y < fListCount; y++)

strFinalXML += "<CHOICE>" + localList.Items[y].Title + "</CHOICE>";

strFinalXML += "</CHOICES></Field>";

break;

default:

break;

}

}

}

}

strFinalXML += "</SurveyQuestions></Survey>";

// Replace any ampersands with proper syntax

strFinalXML = strFinalXML.Replace("&", "&amp;");

A few things to note are that I skip fields in the field collection with the StaticName of "Order" or "MetaInfo", as those fields do not hold information concerning the survey questions. Additionally, the code snippet is not extracting the information from every question type. This was done for brevity in the article, and it is up to the reader to extend the code to handle all question types.

Also note that for lookup question types, I cannot use the SchemaXML information directly, so I am forced to extract the individual attributes I need separately. This is necessary in order to extract all the possible choices that the survey question presents (as the lookup type is a dropdown, radio buttons or check boxes). The GUID of the lookup list is provided, and I am using that to reference the list in SharePoint, in order to iterate through the items in that list, to save them to my XML string.

Once the XML string is created for all the survey questions, we need to do something with that information. I am saving this XML as a document in a document library in SharePoint that I've created (named SurveyDefinitions), as seen below:

try

{

string strSurveyFileName = "FinalSurveyXML.xml";

string onlineSurveySiteRootURL = "http://yoursite.com/";

string surveySiteURL="";

string targetDocLibrary = "Survey Definitions";

using (SPSite site = new SPSite(onlineSurveySiteRootURL))

{

site.AllowUnsafeUpdates = true;

using (SPWeb web = site.OpenWeb(surveySiteURL))

{

web.AllowUnsafeUpdates = true;

SPDocumentLibrary lib = (SPDocumentLibrary)web.Lists[targetDocLibrary];

// Upload survey file to document library (for processing)

// Open and read XML data file into a byte array

System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();

byte[] content = new byte[strFinalXML.Length];

content = encoding.GetBytes(strFinalXML);

SPFileCollection col = web.Files;

SPFile file = col.Add(onlineSurveySiteRootURL + surveySiteURL + targetDocLibrary + "/" + strSurveyFileName, content,true);

file.Update();

web.AllowUnsafeUpdates = false;

}

}

localSurveySite.AllowUnsafeUpdates = false;

}

catch (Exception ex)

{

}

}

Displaying Surveys Offline

Now that we have our XML file for a survey, we need to use it outside of SharePoint. We can create a Windows Forms-based application to display the survey questions, and allow the user to enter responses for the survey.

The main challenge of displaying surveys offline is that we don't know beforehand what questions are in a survey, so any application will have to render the controls associated with the survey questions dynamically. Here is where the XML file generated earlier comes into play. We can iterate through the XML file to determine what type of survey questions are contained within, and render the proper controls dynamically in our application:

// Read XML Definition file and iterate through it, rendering controls

XPathNavigator nav;

XPathDocument docNav;

// Open the XML.

docNav = new XPathDocument(ConfigurationManager.AppSettings["XMLDefinitionFilesLocation"] + strSurveyFile);

// Create a navigator to query with XPath.

nav = docNav.CreateNavigator();

// Initial XPathNavigator to start at the root.

nav.MoveToRoot();

// Move to the first child node

nav.MoveToFollowing("SurveyQuestions", "");

// Find the first element (must be 'SurveyQuestions')

if ((nav.NodeType == XPathNodeType.Element) && (nav.Name == "SurveyQuestions"))

{

nav.MoveToFirstChild();

yPos = yCtlSpacerValue;

xCtlMaxWidthValue = (pnlContainer.Width – xLeftSpacerValue – xRightSpacerValue);

// Iterate through Fields listed

do

{

// Get control info from XML file

strControlType = nav.GetAttribute("Type", "");

strDisplayName = nav.GetAttribute("DisplayName", "");

// Render label control for question text

lblQuestion = new Label();

lblQuestion.Name = "lbl" + strDisplayName;

lblQuestion.Text = strDisplayName;

lblQuestion.AutoSize = false;

// Add the question text label to the form

pnlContainer.Controls.Add(lblQuestion);

lblQuestion.Location = new System.Drawing.Point(xLeftSpacerValue, yPos);

lblQuestion.Size = lblQuestion.GetPreferredSize(new Size(xCtlMaxWidthValue, yCtlHeightValue));

yPos += (lblQuestion.Height + yCtlSpacerValue);

switch (strControlType)

{

case "Text":

bRequired = Convert.ToBoolean(nav.GetAttribute("Required", ""));

if (nav.GetAttribute("MaxLength", "").Trim() != "")

nMaxLength = Convert.ToInt32(nav.GetAttribute("MaxLength", ""));

else

nMaxLength = -1;

txtField = new RegExTextBox();

txtField.Required = bRequired;

txtField.Name = nav.GetAttribute("DisplayName", "");

if (nMaxLength != -1)

txtField.MaxLength = nMaxLength;

txtField.Multiline = false;

txtField.Tag = strControlType; // Holds the control type

txtField.ThemeName = ConfigurationManager.AppSettings["TelerikControlThemeName"];

txtField.Validating += new CancelEventHandler(ControlFields_Validating);

// Add the new control to the form

pnlContainer.Controls.Add(txtField);

txtField.Size = new System.Drawing.Size(xCtlMaxWidthValue, yCtlHeightValue);

txtField.Location = new System.Drawing.Point(xLeftSpacerValue, yPos);

// Set ErrorProvider Icon location to be left of the control

this.errorProvider1.SetIconAlignment(txtField, System.Windows.Forms.ErrorIconAlignment.MiddleLeft);

// Increment y position for next control

yPos += (yCtlHeightValue + yCtlSpacerValue);

break;

default:

break;

}

} while (nav.MoveToNext());

}

else

{

// Throw exception stating XML Definition file is not in correct format

throw new Exception("Definition file is not in the correct format. File=" + strSurveyFile);

}

Our application also allows the user to save his/her responses for the survey they have selected, and this response information is saved to a different XML file. Please note that for lookup lists, radio buttons and multiple selection checkboxes, there is a specific format that you must follow when saving your responses to the XML file. This is necessary when importing the survey responses back into SharePoint, which I will describe in the next section.

// This method retrieves the survey question responses (from the controls on the form)

// and saves them to an XML Data file

public static void SaveSurveyResponses(Panel pnlContainer, string strSurveyFile, string strSurveyTitle, string strDestFile, string strCustomerSelected)

{

string strFinalXML = "<Survey>";

string strDateTimeIdentifier = DateTime.Now.ToString("MM-dd-yyyy-mm-ss");

string strLocalSurveyTitle;

// Strip out unique identifier (after the '_')

if (strSurveyTitle.IndexOf("_") > 0)

{

strLocalSurveyTitle = strSurveyTitle.Substring(0, strSurveyTitle.IndexOf("_"));

strDateTimeIdentifier = strSurveyTitle.Substring((strSurveyTitle.IndexOf("_") + 1));

}

else

strLocalSurveyTitle = strSurveyTitle;

// Retrieve list of survey questions, and calc max label width needed

List<string> strQuestions = SurveyInfo.GetSurveyQuestions(strSurveyFile);

XmlDocument xmlFinalDoc = new XmlDocument();

strFinalXML += "<DefinitionFile>" + strSurveyFile + "</DefinitionFile>";

strFinalXML += "<Title>" + strLocalSurveyTitle + "</Title>";

strFinalXML += "<UniqueName>" + strLocalSurveyTitle + "_" + strCustomerSelected + "</UniqueName><SurveyQuestions>";

#region Create XML for Controls

// All questions and info are contained in controls; just need to iterate through them

foreach (Control c in pnlContainer.Controls)

{

// Do not process labels

if (!(c is Label))

{

string strControlType = c.Tag.ToString();

// Single, multiple line textbox question; also number or currency textbox

if (c is RadTextBox)

{

RadTextBox localControl = (RadTextBox)c;

strFinalXML += "<Field Type=\"" + strControlType + "\" DisplayName=\"" + SurveyInfo.Encode(localControl.Name) + "\" ";

// Question is single line textbox

if (strControlType == "Text")

strFinalXML += "MaxLength=\"" + localControl.MaxLength.ToString() + "\">";

else

strFinalXML += ">";

// Add field value to XML string

strFinalXML += SurveyInfo.Encode(localControl.Text) + "</Field>";

}

// Choice – Listbox questions (multichoice lookup question)

if (c is RadListBox)

{

RadListBox localControl = (RadListBox)c;

// Question is Lookup

if (strControlType == "Lookup")

{

strFinalXML += "<Field Type=\"Lookup\" ";

strFinalXML += "DisplayName=\"" + SurveyInfo.Encode(localControl.Name) + "\">";

foreach (RadListBoxItem rlbi in localControl.SelectedItems)

{

//1;#11102;#6;#11204;#7;#11210

//strFinalXML += localControl.SelectedIndex.ToString() + ";#" + SurveyInfo.Encode(localControl.Text) + "</Field>";

strFinalXML += rlbi.Value.ToString() + ";#" + SurveyInfo.Encode(rlbi.Text) + ";#";

}

// Remove final ";#' from string

strFinalXML = strFinalXML.Remove((strFinalXML.Length – 2), 2);

strFinalXML += "</Field>";

}

}

}

}

strFinalXML += "</SurveyQuestions></Survey>";

// Replace any ampersands with proper syntax, _x0020_ with a space

strFinalXML = strFinalXML.Replace("_x0020_", " ");