- Survey Repository
- Survey Development Studio
- PocketSurvey
- Moving On
PocketSurvey
So far we've seen some of the code that drives the Web service: the back-end code, the database tier, and the code that makes the Windows Forms application possible. Now let's take a look at some of the code that was written for the PocketSurvey application.
As we discussed in Chapter 2, "Using the Survey Development Suite," PocketSurvey is an application that loads a survey profile and then lets people enter their responses. It is designed to take responses to the same survey profile over and over again, assisting survey administrators in conducting a survey run in remote locations such as shopping malls, outside movie theaters, and so on.
The two things that make this application possible are dynamically generated forms and the ability to store and retrieve data on the Pocket PC device.
Code Tour: Dynamic Forms with the DynaFormHelper Class
Dynamically generated forms are possible because of the fact that we can do programmatically everything that the designer can do. This includes creation of controls, placement and sizing of controls, and everything else you want to do with controls. In fact, if you expand the region-collapsed area of C# code that the Visual Studio .NET designer creates for you, you see all the code statements that create everything that the designer has done in the design view.
All the dynamic form creation for the PocketSurvey application is made possible through the use of a helper class. This helper class, as shown in Listing 3.14, is responsible for taking information about a question and creating the appropriate input controls on the form to deal with that question. To do this, it uses four main methods:
Initialize_SingleNumericQuestion
Initialize_NumberListQuestion
Initialize_CheckboxQuestion
Initialize_RadioQuestion
These methods create and position all the input controls necessary to prompt a respondent for the answer to a question. Listing 3.14 shows the DynaFormHelper class.
Listing 3.14 SurveyV1\PocketSurvey\DynaFormHelper.cs The DynaFormHelper Class
using System; using System.Drawing; using System.Windows.Forms; using System.ComponentModel; using System.Collections; using SAMS.Survey.Studio.Library; namespace SAMS.Survey.PocketSurvey { public class DynaFormHelper { private static void InitializeQuestion( frmBlank blank, string question ) { Label lblQuestion = new Label(); lblQuestion.Text = question; lblQuestion.Location = new Point(8, 8); lblQuestion.Width = blank.Width - 10; lblQuestion.Height = 30; blank.Controls.Add( lblQuestion ); } public static void Initialize_SingleNumericQuestion( frmBlank blank, string question ) { InitializeQuestion( blank, question ); TextBox tb = new TextBox(); tb.Width = 32; tb.Location = new Point(8, 40); blank.Controls.Add( tb ); } public static void Initialize_NumberListQuestion( frmBlank blank, string question, string[] options ) { InitializeQuestion( blank, question ); int yOffset = 40; foreach (string option in options) { TextBox tb = new TextBox(); tb.Width = 16; tb.Location = new Point(8, yOffset); blank.Controls.Add( tb ); Label lblOption = new Label(); lblOption.Text = option; lblOption.Location = new Point(30, yOffset +2); yOffset += 22; blank.Controls.Add( lblOption ); } } public static void Initialize_CheckboxQuestion( frmBlank blank, string question, string[] options ) { InitializeQuestion( blank, question ); int yOffset = 40; foreach (string option in options) { CheckBox cb = new CheckBox(); cb.Text = option; cb.Location = new Point(8, yOffset); cb.Checked = false; blank.Controls.Add( cb ); yOffset += 20; } } public static void Initialize_RadioQuestion( frmBlank blank, string question, string[] options ) { InitializeQuestion( blank, question ); int yOffset = 34; Panel pnl = new Panel(); pnl.Location = new Point(0, yOffset); pnl.Height = blank.Height - yOffset - 23; pnl.Width = blank.Width; blank.Controls.Add(pnl); foreach (string option in options) { RadioButton rb = new RadioButton(); rb.Text = option; rb.Checked = false; rb.Location = new Point(8, yOffset); pnl.Controls.Add( rb ); yOffset += 20; } } public static void Initialize_DynaForm( frmBlank blank, QuestionType questionType, string question, string[] options ) { blank.QuestionType = questionType; switch (questionType) { case QuestionType.ChoiceListMultipleAnswers: // this should be a checkbox next to each response option Initialize_CheckboxQuestion( blank, question, options ); break; case QuestionType.ChoiceListNumericalRank: // this should be a text box next to each response option Initialize_NumberListQuestion( blank, question, options ); break; case QuestionType.Essay: // there should be no initialization for essay questions break; case QuestionType.MultipleChoiceSingleAnswer: Initialize_RadioQuestion( blank, question, options ); break; case QuestionType.SingleResponseNumericalRank: Initialize_SingleNumericQuestion(blank, question ); break; } } } }
Now that we've looked at the helper class, let's take a look at the code that makes use of the helper class. When the PocketSurvey application starts up, there is a Take Survey button on the main form. When you click this button, the event handler in Listing 3.15 is triggered.
Listing 3.15 SurveyV1\PocketSurvey\frmMain.cs The Take Survey Button Click Event Handler
private void btnStart_Click(object sender, System.EventArgs e) { SAMS.Survey.Studio.Library.QuestionType questionType; string questionText; DataRow newSheet = dsRun.Tables["ResponseSheets"].NewRow(); newSheet["DateEntered"] = DateTime.Now; newSheet["Source"] = "PocketSurvey"; dsRun.Tables["ResponseSheets"].Rows.Add( newSheet ); // for each question in the profile, prompt the user for a response // then store the responses in a new run foreach ( DataRow question in dsProfile.Tables["Questions"].Rows ) { // obtain the question type questionType = (SAMS.Survey.Studio.Library.QuestionType)((int)question["Type"]); questionText = (string)question["LongText"]; int choiceListId = (int)question["ChoiceListId"]; string[] options = null; if (choiceListId > 0) { DataRow[] items = dsProfile.Tables["ChoiceListItems"].Select("ChoiceListId=" + choiceListId.ToString()); options = new string[ items.Length ]; int i=0; foreach (DataRow item in items) { options[i] = item["Description"].ToString(); i++; } } // grab the options (if any) frmBlank blankForm = new frmBlank(); blankForm.Text = question["ShortDescription"].ToString(); DynaFormHelper.Initialize_DynaForm( blankForm, questionType, questionText, options ); if (blankForm.ShowDialog() == DialogResult.OK) { // do something with the result string response = blankForm.Response; DataRow newResponse = dsRun.Tables["Responses"].NewRow(); newResponse["QuestionId"] = (int)question["ID"]; newResponse["SheetId"] = (int)newSheet["SheetId"]; newResponse["ResponseData"] = response; dsRun.Tables["Responses"].Rows.Add( newResponse ); } } dsRun.AcceptChanges(); dsRun.WriteXml( @"\Program Files\PocketSurvey\CurrentRun.svr" ); } }
In a couple pieces of Listing 3.15, it might not be immediately obvious what is going on. Before this button is even clicked, the survey profile has been loaded from disk (you'll see this code in the next section), and if there is data existing for the current run, that information has also been loaded from disk.
The basic logic is this: For each question loaded in the profile, a dynamic form is created and displayed, the response to that form is then stored, and the form is destroyed when the user clicks Next.
Code Tour: Data Storage
Data storage gave me a bit of a challenge that I wasn't expecting. Having spent so much of my time with .NET using typed data sets, I naturally assumed that I would be able to use typed data sets within the Compact Framework.
The truth is that typed data sets are not supported in the Compact Framework. Even if you attempt to take the class generated by a standard typed data set and compile it within the Compact Framework, it won't work. Getting something resembling a typed data set to work involves a considerable amount of time and effort. Another issue I had was that you can't automatically reuse assemblies between the .NET Framework and the Compact Framework. The reason for this is that the references are strongly typed. What this boils down to is that if your .NET Framework assembly references System.Data, it references a nonportable, platform-specific System.Data namespace. When you build a Compact Framework project, it might also reference System.Data, but that assembly is not the same assembly as the System.Data namespace referenced by a standard Windows Forms application. If you attempt to use an assembly with a platform-specific assembly reference on a mobile device, at best it will simply not load, and at worst it could cause your application to fail.
All I wanted was to be able to verify that the XML that I was loading was of a format that guaranteed my software the capability to load a profile and take response sheets. For that, I could do with some simple schema validation.
Because I didn't want to have any more file dependencies than necessary, I took the XSD that I used to build the typed data set for Survey Development Studio and embedded it directly in the assembly for the PocketSurvey application. In Listing 3.16 you'll see some of the initialization code from PocketSurvey, including the code that creates a schema-backed data set from the assembly-embedded XSD.
Listing 3.16 SurveyV1\PocketSurvey\frmMain.cs The Initialization Code for PocketSurvey
public frmMain() { InitializeComponent(); Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream( "SAMS.Survey.PocketSurvey.SurveyProfile.xsd"); XmlTextReader xr = new XmlTextReader( s ); dsProfile = new DataSet(); dsProfile.ReadXmlSchema( xr ); xr.Close(); s.Close(); dsRun = new DataSet(); s = Assembly.GetExecutingAssembly().GetManifestResourceStream( "SAMS.Survey.PocketSurvey.SurveyRun.xsd"); xr = new XmlTextReader( s ); dsRun.ReadXmlSchema( xr ); xr.Close(); s.Close(); dsProfile.ReadXml( @"\Program Files\PocketSurvey\CurrentSurvey.svp" ); lblCurrent.Text = "Current Profile: " + dsProfile.Tables["Profile"].Rows[0]["Title"].ToString() + "\n\r" + dsProfile.Tables["Profile"].Rows[0]["Notes"].ToString(); if (File.Exists( @"\Program Files\PocketSurvey\CurrentRun.svr" ) ) { dsRun.ReadXml( @"\Program Files\PocketSurvey\CurrentRun.svr" ); } }
In Listing 3.16 you can see that we're using GetManifestResourceStream to take hold of the embedded XSD file that we compiled into the assembly. Then a data set is created, and that schema is read into the profile data set. This ensures that no attempt to load invalid XML will ever succeed. If the load succeeds, the data conforms to the format and requirements of a proper survey profile.
If a current run (that is, collection of response sheets) is found in the application directory, the contents are loaded into memory. This is done because each time someone supplies a response to the system, the data set (dsRun) is written to disk.