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:
{
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("&", "&");
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_", " ");