I had a recent project were I needed to replicate a SharePoint (SP) list and associate them with several Content Types (CT). Mostly, the differences between the CTs were two sets of choice fields (Check boxes). Each set of choice fields (Before and After) had their own individual set of choices, hence the initial reason for using CTs. Initially I felt migrating to InfoPath (IP) and using CTs were unnecessary and useless in SP 2010. Useless because SP 2010 does not support grouping by CT anymore, Thank you Microsoft! So, I could create a field, promote it as a work around as a grouping by CT. Now the only other issue is the before and after choice fields. Do I want to create a view for each form? Do I want to create sections I can dynamically display based on the form type? Actually the answer for both is no. I started researching to see if there was a way to populate the check boxes on a form. The only solution I was able to come up with was a repeating section. Initially this is how I began. I created multiple sections for each form, added a repeating section, and then set default values for my checkbox labels or text. The first time I needed to change these default values it was a major fail. So now I started thinking how could I manage the choices using SP. That’s right, SP. I knew if I created a list I could add it as a data source. Thank to Microsoft again, in InfoPath (IP) 2010 you can no longer use rule to populate a SP list in an IP repeating table/section. In this post I will describe how I setup my SP list, data connections and the little bit of code I needed to complete this task.
Let me begin with my SP list. I created a simple custom list, with a default column “Title”. You have two options with this. You can set the Title field to not be required, or populate it with something, I used numbers, just because it was easy to set them, and I am all about easy.
Earlier I stated I had two choice fields for each form, my list will contain 2 columns for each choice field, four for each CT. One column containing the default value of the check option (Y,N,NA) and the second column will be the label or text for the option. The naming convention I used for my columns match my CTs and are as such:
Acid
- AcidBeforeDefault – default value of blank,Y,N,NA – Choice
- AcidBefore – Option label or text – Single line of text
- AcidAfterDefault – default value of blank,Y,N,NA – Choice
- AcidAfter – Option label or text – Single line of text
This type of naming and setup allows you to create a list of checkboxes and set a default value if needed. Maintenance on this type of setup can be a little confusing and I will explain why. Using Datasheet view, SPs design is set up to associate an item by row, not column, but these checkboxes are sending data to IP by column. So, if I wanted to remove an option I CANNOT delete a row. It would impact all other check boxes. To delete an option, the cell contents need to be deleted. From here you can manage the blank items in your IP repeating section using rules on the form or move the column contents up to replace the deleted item. Remember the Default value column for that label must have the same actions so default values are not changed. This list should look similar to the following:
Fig.1
As you can see I added some instruction to the page for the managing user. It won’t prevent mistakes, but by adding Approval and Versioning we can avoid disaster.
Form setup
My first iteration of the form I had created sections and set default values for all the CTs. When the first request for change came in I knew there had to be a better way. I knew I could pull data from SP and populate an IP repeating table. The question was how can I accomplish this dynamically, so I don’t need a section in the schema for each CT. In order to populate the form from SP I needed to go to a coded solution. So I decided why not add a couple more lines of code which will allow me to populate fields on form load, CT being one of the fields.
I created a field in my schema called ContentType. In the form load options I will search the URL for a query string parameter and set that value to the form field, ContentType.
XPathNavigator root = CreateNavigator();
string TRValue = "Nothing";
XPathNavigator TRnode = root.SelectSingleNode("/my:myFields/my:ContentType", NamespaceManager);
I start by setting my parameter to “Nothing”, and create my XPathNavigator to my field. Now I have a value and a field I can set. TR is my query string parameter and if is not available I set it to nothing. Either way I have a value to set my field with. Below is the C# code to grab the query string value.
try
{
///////////////////////
if (e.InputParameters.ContainsKey("TR"))
{
TRValue = e.InputParameters["TR"];
}
else
{
TRValue = defaultvalue;
}
if (TRnode.Value.Trim() == "")
{
TRnode.SetValue(TRValue);
}
///////////////////////
}
catch {}
This can be place in the FormEvents_Loading for simplicity purposes. By populating the form this way I can add or remove CT without a republish of the form. How’s that for simple?
Form Schema
I have created my form with one main section, which will contain two nested repeating sections for my two checklists. Below is an image of this schema. Again for my naming conventions “Before or Prior” list fields start with “P_” and “After” begins with “A_”. This makes it easier for writing the code. Below display am image of the schema and a breakdown description of the key fields.
Fig.2 |
- displayname = checklist label or question
- YesNo = choice option (Y,N,NA)
- value = this will equal displayname only when current() YesNo value =”Y”. We have a use for this later on where it will be explained.
- NAComments = a comment field for the checklist section
- lbl = has a default value of “Comments:”
- setVal = this is used to hide/show a field specific to 1 CT and one answer.
The fields, in both sections, have the same use and function. For general purposes you only need three of these fields: displayname, YesNo and value. These are the fields I will be focused on in this post.
Now originally I chose a repeating table, but through trial and error I found a repeating section had much more flexibility. This is just a personal preference for me. Populating the data takes the same code and effort. The final look and feel is your call. |
Load Dynamic Checklist
This will take a little more code. Because I am using this multiple times for each CT and multiple CT I needed to make this dynamic also. The module will be passed several strings to populate the correct section (Before or After) with the correct CT checklist choices. In the module I left contents to help identify each string. Source will be SP and Target will be IP. Some of the xpaths to fields may look a bit confusing at first, but to make it dynamic I needed to pass this data in. Also when using the Xmlwriter set all fields with a value even if not needed or they will not populate in the schema. It can be set to blank or string.Empty, it just has to be set. I was trying to execute some rules and write values to the other fields and it took me half a day to realize that if I didn’t add the additional fields they really weren’t there.
public void LoadChecklist(string sourcelbl, string sourcedefault, string targetlbl, string targetdefault, string targetVal, string targetComment, string target_lbl, string targetsetVal, string xpathParent, string xpathRow)
{
XPathNavigator root = CreateNavigator();
//sourcelbl = "Prior" + CT;
//sourcedefault = "Prior" + CT + "Default";
//targetlbl = "field1";
//targetdefault = "field2";
//xpathParent = "/my:myFields/my:group1";
//repeatRow = "groupRepeat";
//Namespace variable
string myNamespace = NamespaceManager.LookupNamespace("my");
//Clear previous entries
XPathIterator tableRows = root.Select(xpathParent + "/my:" + xpathRow, NamespaceManager);
if (tableRows.Count > 0)
{
for (int i = tableRows.Count; i > 0; i--)
{
XPN reTableRows = root.SelectSingleNode(xpathParent + "/my:" + xpathRow + "[" + i + "]", NamespaceManager);
reTableRows.DeleteSelf();
}
}
//Secondary data source setup
DataSource ds = DataSources["Checklist"];
XPathNavigator domNav = ds.CreateNavigator();
XPathIterator rows = domNav.Select("/dfs:myFields/dfs:dataFields/d:SharePointListItem_RW", NamespaceManager);
//Loop through the secondary data source SP
while (rows.MoveNext())
{
string lblVal = rows.Current.SelectSingleNode("d:" + sourcelbl, NamespaceManager).Value.ToString();
string defaultVal = rows.Current.SelectSingleNode("d:" + sourcedefault, NamespaceManager).Value.ToString();
//string id = rows.Current.SelectSingleNode("d:ID", NamespaceManager).Value.ToString();
//Populate the repeating table
using (XmlWriter writer = root.SelectSingleNode(xpathParent, NamespaceManager).AppendChild())
{
writer.WriteStartElement(xpathRow, myNamespace);
writer.WriteElementString(targetlbl, myNamespace, lblVal);
writer.WriteElementString(targetdefault, myNamespace, defaultVal);
writer.WriteElementString(targetVal, myNamespace, "");
writer.WriteElementString(targetComment, myNamespace, "");
writer.WriteElementString(target_lbl, myNamespace, "Comments:");
writer.WriteElementString(targetsetVal, myNamespace, "0");
writer.WriteEndElement();
writer.Close();
}
}
}
String Parameters
- sourcelbl = required, the actual text/question for the option. It is pulled from the SP list
- sourcedefault = required, option default value. It is pulled from the SP list. Sets default option values
- targetlbl = required, option text/question field on form
- targetdefault = required, option value, if any, on the form
- targetVal = required, set a default value of “”
- targetComment = optional, set a default value of “”
- target_lbl = optional, set a default value of “Comments:”
- targetsetVal = optional, set a default value of “0”
- xpathParent = Section – Parent – xpath
- xpathRow = Repeating Row – name of repeating row item i.e. “P_Unload”
Fig.3
Set up code to accept parameters
So now we need to set this up so we pass the correct parameters. I am also doing a check to make sure it is the first time the form is running, or has been saved. We will be using the value of this field to build some of the field strings. In the code below you can see I do my check, get my ContentType value, before the “_” and remove any spaces. This value is now used in my strings.
public void ContentType_Changed(object sender, XmlEventArgs e)
{
string eValue = e.NewValue;
string sub = string.Empty;
string CT = string.Empty;
string SaveCount = root.SelectSingleNode("//my:SaveCount", NamespaceManager).Value;
if (SaveCount == "0")
{
int index = eValue.IndexOf('-');
if (index >= 0)
{
sub = eValue.Substring(0, index);
}
else
{
sub = eValue; // handle strings without the dash
}
CT = sub.Replace(" ", "").Replace(" ", "").Trim();
LoadChecklist("Prior" + CT, "Prior" + CT + "Default", "P_displayname", "P_YesNo", "P_value", "P_NAComments", "P_lbl", "P_setVal", "/my:myFields/my:Checklist/my:P_Unloading", "P_Unload");
LoadChecklist("After" + CT, "After" + CT + "Default", "A_displayname", "A_YesNo", "A_value", "A_NAComments", "A_lbl", "A_setVal", "/my:myFields/my:Checklist/my:A_Unloading", "A_Unload");
}
}
A breakdown
- sourcelbl = “Prior” + CT – SP column name
- sourcedefault = “Prior” + CT + “Default” – SP column name
- targetlbl = P_displayname – form or field name
- targetdefault = P_YesNo – form or field name
- optional form fields
- P_value
- P_NAComments
- P_lbl
- P_setVal
- xpathParent = /my:myFields/my:Checklist/my:A_Unloading
- xpathRow = P_Unload
Loading the form
Now we needed to load the form data and be careful not to over write my checklist when it is opened for editing. The LoadChecklist module above needs to fire only once to get all my values on initial load and not run again. Using the field ContentType as my firing event I am able to run this once. On form load I set that value if it is blank. On the change event of this field I run the above code which gives us the desired results.