Creating Offline Surveys in SharePoint 2010

Author by John Adali

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 the BaseType of 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 = ""; strFinalXML += "" + localSurveyList.Title + ""; strFinalXML += "" + localSurveyList.DefaultViewUrl + ""; strFinalXML += "" + localSurveyList.DefaultViewUrl + ""; // 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 += ""; // 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 += ""; for (y = 0; y < fListCount; y++) strFinalXML += "" + localList.Items[y].Title + ""; strFinalXML += ""; break; default: break; } } } } strFinalXML += ""; // 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 = ""; 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 strQuestions = SurveyInfo.GetSurveyQuestions(strSurveyFile); XmlDocument xmlFinalDoc = new XmlDocument(); strFinalXML += "" + strSurveyFile + ""; strFinalXML += "" + strLocalSurveyTitle + ""; strFinalXML += "" + strLocalSurveyTitle + "_" + strCustomerSelected + ""; #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 += ""; else strFinalXML += ">"; // Add field value to XML string strFinalXML += SurveyInfo.Encode(localControl.Text) + ""; } // Choice - Listbox questions (multichoice lookup question) if (c is RadListBox) { RadListBox localControl = (RadListBox)c; // Question is Lookup if (strControlType == "Lookup") { strFinalXML += ""; foreach (RadListBoxItem rlbi in localControl.SelectedItems) { //1;#11102;#6;#11204;#7;#11210 //strFinalXML += localControl.SelectedIndex.ToString() + ";#" + SurveyInfo.Encode(localControl.Text) + ""; strFinalXML += rlbi.Value.ToString() + ";#" + SurveyInfo.Encode(rlbi.Text) + ";#"; } // Remove final ";#' from string strFinalXML = strFinalXML.Remove((strFinalXML.Length - 2), 2); strFinalXML += ""; } } } } strFinalXML += ""; // Replace any ampersands with proper syntax, _x0020_ with a space strFinalXML = strFinalXML.Replace("_x0020_", " "); #endregion xmlFinalDoc.LoadXml(strFinalXML); // Save XML Data file to specific filename, if present; // If not, save XML Data file to a time-specific filename if (strDestFile != "") { xmlFinalDoc.Save(ConfigurationManager.AppSettings["XMLDataFilesLocation"] + strDestFile); } else { xmlFinalDoc.Save(ConfigurationManager.AppSettings["XMLDataFilesLocation"] + strLocalSurveyTitle + "_" + strDateTimeIdentifier + ConfigurationManager.AppSettings["XMLDataFileExtension"]); } }

Adding Survey Responses Programmatically

The client object model introduced in SharePoint 2010 can be used to add survey responses to a survey in SharePoint. This is very powerful, as it allows surveys to be rendered offline (using the XML files created earlier), and end users can create responses to a survey while offline, and import these responses back to the SharePoint site when they are online. Our Forms-based application can perform this “uploading” of survey response data for us. Firstly, the application must have connectivity with the SharePoint site. This is a simple matter of detecting the document library we wish to upload our survey response to. Next, to record the response information, our application uploads the survey response file to a document library in SharePoint that has an event receiver listening for items added to it:
// This method uploads one survey file to SharePoint public static bool UploadSingleSurveyToSharePoint(string strSurveyName, string strSurveyFileName, ClientOM.ClientContext localContext) { bool bRetCode = false; ClientOM.ClientContext clientContext; ClientOM.Web site; try { // Only save responses if survey name is selected if (strSurveyFileName.Length > 0) { if (localContext == null) clientContext = new ClientOM.ClientContext(ConfigurationManager.AppSettings["OnlineSurveySiteURL"]); else clientContext = localContext; using (clientContext) { // Use default credentials (currently logged in user) clientContext.Credentials = System.Net.CredentialCache.DefaultCredentials; // Upload survey file to document library (for processing) site = clientContext.Web; ClientOM.List uploadList = site.Lists.GetByTitle("UploadedSurveyResponses"); // Open and read XML data file into a byte array FileStream fstream = File.OpenRead(ConfigurationManager.AppSettings["XMLDataFilesLocation"] + strSurveyFileName); byte[] content = new byte[fstream.Length]; fstream.Read(content, 0, (int)fstream.Length); fstream.Close(); // Setup class for creating a file in SharePoint ClientOM.FileCreationInformation fci = new ClientOM.FileCreationInformation(); fci.Content = content; fci.Overwrite = true; fci.Url = ConfigurationManager.AppSettings["OnlineSurveySiteURL"] + ConfigurationManager.AppSettings["UploadDocumentLibrary"] + @"/" + strSurveyFileName; ClientOM.Folder folder = site.GetFolderByServerRelativeUrl(ConfigurationManager.AppSettings["OnlineSurveySiteURL"] + ConfigurationManager.AppSettings["UploadDocumentLibrary"] + @"/"); ClientOM.File file = folder.Files.Add(fci); // Send commands to SharePoint site for execution clientContext.ExecuteQuery(); // If successful, delete file and refresh dropdown File.Delete(ConfigurationManager.AppSettings["XMLDataFilesLocation"] + strSurveyFileName); bRetCode = true; } } } catch (Exception ex) { bRetCode = false; SurveyInfo.WriteToFileLog(ex.ToString(), "SurveyInfo", "UploadSingleSurveyToSharePoint"); } return bRetCode; } Once a response is uploaded, the code in the event receiver processes the information to add the response into the survey, as seen here: /// /// An item was added /// public override void ItemAdded(SPItemEventProperties properties) { // Only process items added in the UploadedSurveyResponses document library if (properties.List.BaseType == SPBaseType.DocumentLibrary) { if (properties.ListTitle == "UploadedSurveyResponses") { this.EventFiringEnabled = false; base.ItemAdded(properties); try { SPFile file = properties.Web.GetFile(properties.ListItem.File.Url); byte[] contents = file.OpenBinary(); SPListItem liQuestion; string strFileName = properties.ListItem.File.Title; using (Stream s = new MemoryStream(contents)) { XPathNavigator nav; XPathDocument docNav = new XPathDocument(s); s.Close(); // Create a navigator to query with XPath. nav = docNav.CreateNavigator(); //Initial XPathNavigator to start at the root. nav.MoveToRoot(); nav.MoveToFollowing("Title", ""); string strTitle = nav.Value; SPListCollection listColl = properties.Web.Site.RootWeb.GetListsOfType(SPBaseType.Survey); SPList listSurvey = listColl.TryGetList(strTitle); // Only process if survey is found if (listSurvey != null) { //Move to the Title element if (nav.MoveToFollowing("SurveyQuestions", "")) { string strQuestionName; string strControlType; // Create string for survey response liQuestion = listSurvey.Items.Add(); liQuestion["Name"] = strFileName; // Loop through all questions while (nav.MoveToFollowing("Field", "")) { strQuestionName = nav.GetAttribute("DisplayName", ""); strControlType = nav.GetAttribute("Type", ""); SPFieldType fieldType; switch (strControlType) { case "Text": fieldType = SPFieldType.Text; break; case "Note": fieldType = SPFieldType.Note; break; default: fieldType = SPFieldType.Text; break; } // Create string for survey response liQuestion[strQuestionName] = Decode(nav.Value.ToString()); } properties.Web.AllowUnsafeUpdates = true; // Add response to survey liQuestion.Update(); properties.Web.AllowUnsafeUpdates = false; } } else { // throw exception back to calling method throw new Exception("Survey not found in " + strFileName); } } } catch (Exception ex) { } this.EventFiringEnabled = true; } } }
Please note that for lookups, radio buttons, and multiple-selection checkboxes, the format of the response is important, as that specifies to the survey which selection item the response data corresponds to. The format is: ;#;#;#;#;# If the index number is incorrect, a different response value will be recorded with the survey response, as SharePoint uses that number to determine which selection item to include as the response.

Conclusion

The new client object model for SharePoint 2010 has allowed surveys to be extended to the offline realm, and opens the door for many exciting scenarios for offline application uses with SharePoint 2010. My scenario of offline surveys is just one scenario, and there are numerous enhancements that can be done to extend this scenario even further.
Author

John Adali

Senior Software Developer - Modern Applications