If you have ever attempted to sort repeating node data (i.e. a Repeating Table or Repeating Section) on an InfoPath form, you will find this functionality is not available through the UI. However, using .NET classes you can easily implement a sorting routine that will work in both client and browser scenarios. We discussed one way to make this happen through custom code in a recent article; this post will show a different way to make it happen. We will take a look at how to implement this functionality along with taking advantage of some new features in InfoPath 2007:
In this sample scenario, let’s assume you are capturing the following data in a Repeating Table:
In addition, you want to allow your users to select the field they want to sort on (using a button in the column header) and clicking the button will toggle the sort between Ascending and Descending. Here is a sample form showing those options:
** NOTE: Notice the “(Asc)” in the Last Name button label? We’ll show you how to do that at the end of this post!
The data structure for the above sample is as follows:
So in this scenario, the user would simply click the button above the field they want to use to sort the data and by default, the first click would sort the data in Ascending order and clicking it again would sort the data in Descending order. Let’s now go ahead and take a look at the code on the click event of the buttons that implements this functionality.
When each button is clicked, the first thing we do is set the value of the SortOrder and SortField nodes. For ease of implementation, we created a “SpecifySortOptions” procedure that is called from the click event of each button:
SpecifySortOptions("LastName",XmlDataType.Text,e.ControlId);
When each button is clicked we call this procedure passing it the field we want to use for sorting (in this case, LastName), the data type of this field (XmlDataType.Text) and the ControlID of the button that was clicked. (The ControlID is used in the Expression for the Default Value property of each button to determine how to change the label.)
Here is the SpecifySortOptions procedure:
public void SpecifySortOptions(string SortField, XmlDataType dataType, string ControlID) { //Create Navigator objects for the main DOM and //for the SortOrder and SortField fields XPathNavigator xn = this.MainDataSource.CreateNavigator(); XPathNavigator xnSortOrder = xn.SelectSingleNode("/my:myFields/my:SortOrder", this.NamespaceManager); XPathNavigator xnSortField = xn.SelectSingleNode("/my:myFields/my:SortField", this.NamespaceManager); //Check to see if the value of the SortField is equal //to the ControlID that we passed to this procedure. If //it is the same and the SortOrder field is an SortOrder //emptry string or is set to "Desc" then set the field to //"Asc". If the SortField value does not equal the //ControlID that we passed to this procedure, then that //would mean either the SortField is an empty string or //it was set to another field - either way, we will //then want the SortOrder value to be "Asc" if (xnSortField.Value == ControlID) { if (xnSortOrder.Value == "" || xnSortOrder.Value == "Desc") xnSortOrder.SetValue("Asc"); else xnSortOrder.SetValue("Desc"); } else xnSortOrder.SetValue("Asc"); //Call the SortTheData() procedure passing in the values //specified above SortTheData(SortField, xnSortOrder.Value, dataType); //Set the SortField value to the current ControlID xnSortField.SetValue(ControlID); }
public void SpecifySortOptions(string SortField, XmlDataType dataType, string ControlID)
{
//Create Navigator objects for the main DOM and
//for the SortOrder and SortField fields
XPathNavigator xn = this.MainDataSource.CreateNavigator();
XPathNavigator xnSortOrder = xn.SelectSingleNode("/my:myFields/my:SortOrder", this.NamespaceManager);
XPathNavigator xnSortField = xn.SelectSingleNode("/my:myFields/my:SortField", this.NamespaceManager);
//Check to see if the value of the SortField is equal
//to the ControlID that we passed to this procedure. If
//it is the same and the SortOrder field is an SortOrder
//emptry string or is set to "Desc" then set the field to
//"Asc". If the SortField value does not equal the
//ControlID that we passed to this procedure, then that
//would mean either the SortField is an empty string or
//it was set to another field - either way, we will
//then want the SortOrder value to be "Asc"
if (xnSortField.Value == ControlID)
if (xnSortOrder.Value == "" || xnSortOrder.Value == "Desc")
xnSortOrder.SetValue("Asc");
else
xnSortOrder.SetValue("Desc");
}
//Call the SortTheData() procedure passing in the values
//specified above
SortTheData(SortField, xnSortOrder.Value, dataType);
//Set the SortField value to the current ControlID
xnSortField.SetValue(ControlID);
After calling the SpecifySortOptions procedure from the click event of each button, this procedure calls the SortTheData procedure, which accepts a string value for the sort field (strSortField), a string value for the sort order (strSortOrder) and an XmlDataType value (dataType) for the type of data being sorted. This is the code that will actually perform the sorting.
The first thing we need to do is this procedure is create “XPathNavigator” objects for the main DOM:
//Create a Navigator object for the main DOM XPathNavigator xn = this.MainDataSource.CreateNavigator();
//Create a Navigator object for the main DOM
We then will create an XmlSortOrder object so we can specify either an Ascending or Descending sort. In this sample, we will specify an Ascending sort as the default; however, we will check the value of strSortOrder and if this is set to “Desc”, change the XmlSortOrder object accordingly:
XmlSortOrder sortOrder = XmlSortOrder.Ascending; if (strSortOrder == "Desc") sortOrder = XmlSortOrder.Descending;
XmlSortOrder sortOrder = XmlSortOrder.Ascending;
if (strSortOrder == "Desc")
sortOrder = XmlSortOrder.Descending;
To actually perform the sort, we will be using the “AddSort” method of an XPathExpression object – as such, we need to create an XPathExpression object for the repeating (group) node that we are going sort:
XPathExpression xe = xn.Compile("/my:myFields/my:group1/my:group2");
Now we can use the AddSort method on the Expression object using the field name (strSortField) that we passed into this procedure, the sort order using the sort order object (sortOrder) we created above and the data type using the data type object (dataType) we passed into this procedure:
xe.AddSort("*[local-name()='" + strSortField + "']", sortOrder, XmlCaseOrder.None, "", dataType);
We need to specify a NamespaceManager for the Expression object and for this we will use the SetContext method:
xe.SetContext(this.NamespaceManager);
The next step is to create an XPathNodeIterator object, passing it our XPathExpression object, so we can iterate all the nodes now that they are sorted - in addition, we will use this object (in the lastNode expression below) to get a count of the total nodes in this repeating group:
XPathNodeIterator xi = xn.Select(xe);
In the end, the way this procedure works is to delete the existing “un-sorted” nodes and add back the “sorted” nodes via the XPathNodeIterator object. So the next step is to now delete the existing “un-sorted” data. To do this, we will create XPathNavigator objects to reference the first and last nodes in this repeating group and then use the DeleteRange method to delete those nodes:
XPathNavigator firstNode = xn.SelectSingleNode("/my:myFields/my:group1/my:group2[1]", this.NamespaceManager); XPathNavigator lastNode = xn.SelectSingleNode("/my:myFields/my:group1/my:group2[" + xi.Count + "]", this.NamespaceManager); firstNode.DeleteRange(lastNode);
XPathNavigator firstNode = xn.SelectSingleNode("/my:myFields/my:group1/my:group2[1]", this.NamespaceManager);
XPathNavigator lastNode = xn.SelectSingleNode("/my:myFields/my:group1/my:group2[" + xi.Count + "]", this.NamespaceManager);
firstNode.DeleteRange(lastNode);
At this point, we have the sorted data in memory and the un-sorted data has been removed so we are ready to add that sorted data back to the form. For this process, we will use the XPathNodeIterator object we created earlier to iterate over the nodes.
while (xi.MoveNext()) { //Create string variables to hold the values of each field //as we iterate the nodes string strLastName = xi.Current.SelectSingleNode("my:LastName", this.NamespaceManager).Value; string strFirstName = xi.Current.SelectSingleNode("my:FirstName", this.NamespaceManager).Value; string strAge = xi.Current.SelectSingleNode("my:Age", this.NamespaceManager).Value; //Call the AddNewRow method to append a new row // to the repeating group AddNewRow(xn.SelectSingleNode("/my:myFields/my:group1", this.NamespaceManager)); //Since we are continually appending new rows, the //"last" row will always be the one where we need //to set the values - so here we will create a //Navigator object for this newly added row - we //will use this for setting the field values below XPathNavigator xnNewRow = xn.SelectSingleNode("/my:myFields/my:group1/my:group2[last()]", this.NamespaceManager); xnNewRow.SelectSingleNode("my:LastName", this.NamespaceManager).SetValue(strLastName); xnNewRow.SelectSingleNode("my:FirstName", this.NamespaceManager).SetValue(strFirstName); //Since the Age field is numeric, it will contain //the "nil" attribute. We need to remove this //arrtibute prior to programmatically setting the //value. To do this, we'll call the DeleteNil //procedure passing it the node that contains //(or may contain) the nil attribute DeleteNil(xnNewRow.SelectSingleNode("my:Age", this.NamespaceManager)); //Now we can set the value of the Age field xnNewRow.SelectSingleNode("my:Age", this.NamespaceManager).SetValue(strAge); }
while (xi.MoveNext())
//Create string variables to hold the values of each field
//as we iterate the nodes
string strLastName = xi.Current.SelectSingleNode("my:LastName", this.NamespaceManager).Value;
string strFirstName = xi.Current.SelectSingleNode("my:FirstName", this.NamespaceManager).Value;
string strAge = xi.Current.SelectSingleNode("my:Age", this.NamespaceManager).Value;
//Call the AddNewRow method to append a new row
// to the repeating group
AddNewRow(xn.SelectSingleNode("/my:myFields/my:group1", this.NamespaceManager));
//Since we are continually appending new rows, the
//"last" row will always be the one where we need
//to set the values - so here we will create a
//Navigator object for this newly added row - we
//will use this for setting the field values below
XPathNavigator xnNewRow = xn.SelectSingleNode("/my:myFields/my:group1/my:group2[last()]", this.NamespaceManager);
xnNewRow.SelectSingleNode("my:LastName", this.NamespaceManager).SetValue(strLastName);
xnNewRow.SelectSingleNode("my:FirstName", this.NamespaceManager).SetValue(strFirstName);
//Since the Age field is numeric, it will contain
//the "nil" attribute. We need to remove this
//arrtibute prior to programmatically setting the
//value. To do this, we'll call the DeleteNil
//procedure passing it the node that contains
//(or may contain) the nil attribute
DeleteNil(xnNewRow.SelectSingleNode("my:Age", this.NamespaceManager));
//Now we can set the value of the Age field
xnNewRow.SelectSingleNode("my:Age", this.NamespaceManager).SetValue(strAge);
In the while loop above, we used the “AddNewRow” and “DeleteNil” procedures – these are documented below:
public void AddNewRow(XPathNavigator docXN) { //Create a Navigator object to reference the node //we will be adding. To do this, we can use the //templates' "Manifest.xsf" file to get the //appropriate node to add. As you can see, this is //specific to the control's "name", which you can //get from the Advanced tab on the Properties window //for the repeating control. Once you have this, //use the "Save As Source Files" command from the //File menu in InfoPath and locate the appropriate //expression in your Manifest.xsf file XPathNavigator xnNode = this.Template.Manifest.SelectSingleNode("//xsf:xDocumentClass/xsf:views/xsf:view/xsf:editing/xsf:xmlToEdit[@name='group2_1']/xsf:editWith/xsf:fragmentToInsert/xsf:chooseFragment/my:group1", this.NamespaceManager); //Append the node from the Manifest file to the main DOM docXN.SelectSingleNode("/my:myFields/my:group1", this.NamespaceManager).AppendChild(xnNode.InnerXml); } public void DeleteNil(XPathNavigator node) { //Check to see if the nil attribute exists //and if so, delete it if (node.MoveToAttribute("nil", http://www.w3.org/2001/XMLSchema-instance)) node.DeleteSelf(); }
public void AddNewRow(XPathNavigator docXN)
//Create a Navigator object to reference the node
//we will be adding. To do this, we can use the
//templates' "Manifest.xsf" file to get the
//appropriate node to add. As you can see, this is
//specific to the control's "name", which you can
//get from the Advanced tab on the Properties window
//for the repeating control. Once you have this,
//use the "Save As Source Files" command from the
//File menu in InfoPath and locate the appropriate
//expression in your Manifest.xsf file
XPathNavigator xnNode = this.Template.Manifest.SelectSingleNode("//xsf:xDocumentClass/xsf:views/xsf:view/xsf:editing/xsf:xmlToEdit[@name='group2_1']/xsf:editWith/xsf:fragmentToInsert/xsf:chooseFragment/my:group1", this.NamespaceManager);
//Append the node from the Manifest file to the main DOM
docXN.SelectSingleNode("/my:myFields/my:group1", this.NamespaceManager).AppendChild(xnNode.InnerXml);
public void DeleteNil(XPathNavigator node)
//Check to see if the nil attribute exists
//and if so, delete it
if (node.MoveToAttribute("nil", http://www.w3.org/2001/XMLSchema-instance))
node.DeleteSelf();
And that’s it! You now have the functionality of sorting data in a repeating node. For reference, the complete code for this sample is attached.
Now – about that button label…how did we do that??!
With InfoPath 2007, we have a new feature that allows you to specify a dynamic value for the button label. To do this, simply click the “fx” button next to the label property and you can choose to use an expression of a field/group from your form:
However, for this sample the conditional logic for the button label is quite complex: we need to determine which button was clicked and whether we should show “(Asc)” or “(Desc)” next to the correct label. For this, we used the process demonstrated in this blog post: Conditional Default Values. So here is the logic that needed to be implemented when each button is clicked; for example, for the Last Name button:
This is the logic that needs to be implemented for each button. Here is a sample expression for the LastName field:
concat(substring("Last Name (Asc)", 1, ((my:SortOrder = "" or my:SortOrder = "Asc") and my:SortField = "btnLastName") * string-length("Last Name (Asc)")), substring("Last Name (Desc)", 1, (my:SortOrder = "Desc" and my:SortField = "btnLastName") * string-length("Last Name (Desc)")), substring("Last Name", 1, not(my:SortField = "btnLastName") * string-length("Last Name")))
Each of the above “substring” expressions are tested in order – so if the SortOrder field does not equal an empty string or does not equal “Asc” and the SortField value does not equal “btnLastName” then we test the next condition. If the SortOrder value does not equal “Desc” and the SortField value does not equal “btnLastName” then we test the last condition. And here we only need to check the value of my:SortField – if this does not equal “btnLastName” then we know a different button was clicked and we only want the label to display “Last Name”.
So there you have it! A way to sort data in your repeating table and a really cool way to let the user know which field they clicked for sorting and in which order the data has been sorted!
** NOTE: It seems we may have a bug with our expression box in that it will accept the entire conditional statement noted above but once you close and re-open the box, the string gets truncated. Once you have this working, you may want to keep that expression saved in a text file.
Scott HeimSupport Engineer
Why would the AppendRow fail "with a non data-type error"?
Found that if the schema was a single level, ie:
my:myFields/my:group1
and you are trying to add the repeating group called Activity, it won't work. You need to have a schema with an intermediate group, followed by the repeating group, like this:
my:myFields/my:group1/my:group2
Then the AppendChild will work.
Hi BobC,
Glad you were able to get it to work.
Scott
I have already given a programmatic solution of sorting a repeating table in InfoPath but here is another
I have not been able to get this to work with Infopath 2007 it just comes back and says that the form cannot be opened.
Is there downloadable example of the form?
Hi spawforths,
I apologize as I am not sure what happened to the attachment with the completed code; however, here are all the procedures as I documented them in this post. If you followed the naming conventions in this sample you should be able to copy and paste the code in each procedure. NOTE: For ease, I named by buttons as follows:
btnLastName
btnFirstName
btnAge
public void btnLastName_Clicked(object sender, ClickedEventArgs e)
SpecifySortOptions("LastName", XmlDataType.Text, e.ControlId);
public void btnFirstName_Clicked(object sender, ClickedEventArgs e)
SpecifySortOptions("FirstName", XmlDataType.Text, e.ControlId);
public void btnAge_Clicked(object sender, ClickedEventArgs e)
SpecifySortOptions("Age", XmlDataType.Text, e.ControlId);
public void SortTheData(string strSortField, string strSortOrder, XmlDataType dataType)
if (node.MoveToAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance"))
Thanks for that, I managed to get to work.
When I try and get it to work by altering the code for another form I struggle.
The form I am trying use is a repeatable table that submits to an SQL database.
My 6 fields are
Project
Project_FD
Date_Worked
Work_Code
Work_Description
Here is my altered code, although it fails on
public void btnPro_Clicked(object sender, ClickedEventArgs e)
SpecifySortOptions("Project", XmlDataType.Text, e.ControlId);
XPathExpression xe = xn.Compile("/my:myFields/dataFields/d:Project_Hours");
XPathNavigator firstNode = xn.SelectSingleNode("/my:myFields/dataFields/d:Project_Hours[1]", this.NamespaceManager);
XPathNavigator lastNode = xn.SelectSingleNode("/my:myFields/dataFields/d:Project_Hours[" + xi.Count + "]", this.NamespaceManager);
string strProject = xi.Current.SelectSingleNode("my:Project", this.NamespaceManager).Value;
string strProject_FD = xi.Current.SelectSingleNode("my:Project_FD", this.NamespaceManager).Value;
string strDate_Worked = xi.Current.SelectSingleNode("my:Date_Worked", this.NamespaceManager).Value;
string strWork_Code = xi.Current.SelectSingleNode("my:Work_Code", this.NamespaceManager).Value;
string strWork_Description = xi.Current.SelectSingleNode("my:Work_Description", this.NamespaceManager).Value;
string strBillable_Hours = xi.Current.SelectSingleNode("my:Billable_Hours", this.NamespaceManager).Value;
AddNewRow(xn.SelectSingleNode("/my:myFields/dataFields", this.NamespaceManager));
XPathNavigator xnNewRow = xn.SelectSingleNode("/my:myFields/dataFields/d:Project_Hours[last()]", this.NamespaceManager);
xnNewRow.SelectSingleNode("my:Project", this.NamespaceManager).SetValue(strProject);
xnNewRow.SelectSingleNode("my:Project_FD", this.NamespaceManager).SetValue(strProject_FD);
xnNewRow.SelectSingleNode("my:Work_Description", this.NamespaceManager).SetValue(strWork_Description);
DeleteNil(xnNewRow.SelectSingleNode("my:Date_Worked", this.NamespaceManager));
DeleteNil(xnNewRow.SelectSingleNode("my:Work_Code", this.NamespaceManager));
DeleteNil(xnNewRow.SelectSingleNode("my:Billable_Hours", this.NamespaceManager));
xnNewRow.SelectSingleNode("my:Date_Worked", this.NamespaceManager).SetValue(strDate_Worked);
xnNewRow.SelectSingleNode("my:Work_Code", this.NamespaceManager).SetValue(strWork_Code);
xnNewRow.SelectSingleNode("my:Billable_Hours", this.NamespaceManager).SetValue(strBillable_Hours);
XPathNavigator xnNode = this.Template.Manifest.SelectSingleNode("//xsf:xDocumentClass/xsf:views/xsf:view/xsf:editing/xsf:xmlToEdit[@name='Project_Hours_1']/xsf:editWith/xsf:fragmentToInsert/xsf:chooseFragment/my:group1", this.NamespaceManager);
docXN.SelectSingleNode("/my:myFields/dataFields", this.NamespaceManager).AppendChild(xnNode.InnerXml);
The error I receive is
nullReferenceException was unhandled by user code
It may simply be your XPATH expressions are not correct. For instance, this looks wrong:
If you have built your XSN from a data source (i.e. SQL), this would typically be like this:
XPathExpression xe = xn.Compile("/my:myFields/dfs:dataFields/d:Project_Hours");
To be sure of the XPATH expressions you can select the node you want in the Data Source Task Pane, right-click and choose Copy XPath.
I would first verify all your expressions and if it still fails, then you will need to debug (step through) your code to find the exact line that is failing and correct that problem.
Hi Scott,
Thanks for that,
I started to change the x-path references, it seems to fail on
vxnNewRow.SelectSingleNode("@Project", this.NamespaceManager).SetValue(strProject);
But it gives me the error: The name ' vxnNewRow' does not exist in the current context.
any idea?
What this error indicates is the "vxnNewRow" variable is not declared at the correct location. Now, if you are using the sample code I provided, then the variable name should be "xnNewRow" - did you change the variable names? If so, then it sounds like it is not declared at the correct location.
The other thing it could be is when you create that variable, it is done on this line:
Maybe in your case, the XPATH expression does not return any rows? If not, when you step through the code and hover your mouse over this variable (after moving past this line) if the "vxnNewRow" variable is "null" then there again is a problem with the expression.