Managing Repeating Data with Code, Part 2: Use a Repeating Table

In case you didn’t read the previous entry in this three-part blog series, one way to manage repeating data with code in InfoPath (and displaying this data to the user) requires nothing more than a listbox and a simple code-behind method that adds a repeating element to the form’s data source for each piece of repeating data.

To demonstrate the need to manage repeating data in InfoPath, we used a “search” use-case scenario in which the user enters a search term, presses a button, and a code-behind method queries a database with the search term and displays the search “results” (rows returned from by the database query) to the user in a useful manner.  These results represent the names and addresses of vendors with which an organization regularly conducts business.

Recap: The listbox scenario

To reiterate, our requirements for useful organization of this data are as follows:

  • Easy to read
  • Easy to navigate (in the event that the search returns lots of data)
  • The user can easily visualize the vendor’s name, code and address
We initially explored utilizing a listbox control that contains pipe-delimited strings representing each vendor code and address.  When a user selects a vendor, the address is parsed out and displayed to the right of the listbox:
Implementation of the search results listbox
One of the largest drawbacks of this method of displaying repeating data comes from the requirement for the user to select a vendor (in the listbox on the left) in order to easily view the more complete chunk of data (in the “envelope” on the right).  Combined with the fact that this implementation is also just plain ugly, I set about finding another way to display search results within an InfoPath form.

Good solution: Use a single repeating table instead of a listbox (hard-coded)

It quickly became apparent that repeating table controls satisfy all of the requirements for displaying search results.  If you have used repeating table controls before, you know that they offer many enhancements over listbox controls.  Repeating table controls let us:

  • Show multiple lines of data
  • Include other controls (buttons, check-boxes, textboxes, etc.)
  • Include graphics
  • Allow “insert and delete” actions (not necessary for our implementation)
It should be noted that they aren’t without their own drawbacks, though.  In browser-enabled forms like the one we’re building:
  • Repeating tables can only repeat vertically
  • They can’t be placed within a scrolling region– this, in my humble opinion, is a major flaw:  the size of the repeating table will grow with the number of search results returned, and thus the form itself will also grow (unacceptable for small forms like ours).

Repeating tables aren’t easily modifiable with code.  If you’ve tried to fill repeating tables in the past using code-behind, you understand that it’s a struggle.  What do we do with the initial element (which is always blank?).  How do we access the value in the table?  Most importantly, how do we code a method that can fill a repeating table as generically as possible, without hard-coding the XPATH to the table?  Read on.

Although not originally my intention, the rest of this post will proceed on a step-by-step basis in an attempt to create an elegant solution for the aforementioned problems affecting repeating tables– specifically, filling them with code-behind– and more specifically, using this methodology to solve our “search results” use-case dilemma.

Adding repeating elements using code

I actually addressed this in the first part of this post, where we added a repeating element called “Vendor_Listbox_Item” for each row returned by a call to a database query.  Take, for instance, the same code posted in the aforementioned post:

//Create an XmlDocument object
XmlDocument doc = new XmlDocument();

//Create Vendor_Listbox_Item element
XmlNode group = doc.CreateElement("Vendor_Listbox_Item", NamespaceManager.LookupNamespace("my"));

//Create Vendor_Listbox_Item_Name field, where we store the data returned from the database query
XmlNode field = doc.CreateElement("Vendor_Listbox_Item_Name", NamespaceManager.LookupNamespace("my")); //Listbox "value" source
XmlNode node = group.AppendChild(field);

//Parse the column values returned from the database row and append them to the data field
node.InnerText = reader["VENDOR_CODE"].ToString() + " | " + reader["ADDR1"].ToString()
    + " || \n" + reader["ADDR2"].ToString() + " ||| \n" + reader["ADDR3"].ToString() + " |||| \n" + reader["ADDR4"].ToString();

//Add the newly-created repeating element to the form's XML data source
MainDataSource.CreateNavigator().SelectSingleNode("//my:myFields/my:Vendor_Listbox", NamespaceManager).AppendChild(doc.DocumentElement.CreateNavigator());

Two important things to note regarding this method of writing repeating data to a repeating XML element:

  1. The XPATH expressions representing the repeating elements are hard-coded, as is the text we’re placing into the repeating elements
  2. The data is packed like sardines into a single field, “Vendor_Listbox_Item_Name”, because we can only display one field in a listbox.  A more elegant solution would store each vendor address line its own field.
We can use the same general approach to write elements to a repeating table.  Below is the data structure that our repeating table control will use.  It has been renamed and reorganized to more accurately fit with the repeating table control that will represent it on the form:
Repeating Element Data Structure
The fields in the “VendorQueryData” element are explained below:
  1. VendorQueryDataName – The short name of the vendor (string).  Example: “Chris’ Bacon Factory”
  2. VendorQueryDataCode – The internal code identifying the vendor (string).  Example: “CBACON11”
  3. VendorQueryDataAddrLine[#] – Line [#] of the vendor’s address (string).  Example: “123 Awesome Street”
  4. VendorQueryDataSelected – Represents whether the element has been selected (boolean); bound to a check-box control.  True or false.
  5. VendorQueryDataPosition – A number corresponding to the position in the table (integer).  Always incremented for each search result added to the table (I’ll explain why we need this a bit later)…
We can create a simple repeating table control by dragging the VendorQueryData repeating element onto the form as a repeating table.  We could “fill” the repeating table with search results by modifying the method posted above– instead of filling a single field with pipe-delimited data, we can fill each field in the repeating element with the proper data.  Note that “i” corresponds to an integer that is incremented for each database row returned:

/*Write a row to a repeating table*/
//Create an XmlDocument object
XmlDocument doc = new XmlDocument();

//Create VendorQueryData element
XmlNode group = doc.CreateElement("VendorQueryData", NamespaceManager.LookupNamespace("my"));

//Create VendorQueryDataName field, where we now store ONE LINE of the data returned by the database query
XmlNode field = doc.CreateElement("VendorQueryDataName", NamespaceManager.LookupNamespace("my"));
XmlNode node = group.AppendChild(field);

//Parse a single column value returned from the database row and append it
node.InnerText = reader["VENDOR_NAME"].ToString();

//Do the same for VendorQueryDataCode
field = doc.CreateElement("VendorQueryDataCode", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = reader["VENDOR_CODE"].ToString();

//VendorQueryDataAddrLine1
field = doc.CreateElement("VendorQueryDataAddrLine1", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = reader["ADDR1"].ToString();

//VendorQueryDataAddrLine2
field = doc.CreateElement("VendorQueryDataAddrLine2", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = reader["ADDR2"].ToString();

//VendorQueryDataAddrLine3
field = doc.CreateElement("VendorQueryDataAddrLine3", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = reader["ADDR3"].ToString();

//VendorQueryDataAddrLine4
field = doc.CreateElement("VendorQueryDataAddrLine4", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = reader["ADDR4"].ToString();

//VendorQueryDataSelected
field = doc.CreateElement("VendorQueryDataSelected", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = "false";

//VendorQueryDataPosition
field = doc.CreateElement("VendorQueryDataPosition", NamespaceManager.LookupNamespace("my"));
node = group.AppendChild(field);
node.InnerText = i.ToString(); //i is incremented for each database row we read

//Add the newly-created repeating element to the form's XML data source
MainDataSource.CreateNavigator().SelectSingleNode("//my:myFields/my:Vendor_Listbox", NamespaceManager).AppendChild(doc.DocumentElement.CreateNavigator());
/*Rinse and repeat*/

But what if we want to do this with other data?  In my case, different form views give the user opportunities to search different databases, and the data sets returned always look different from one another in number of fields, field names, data types, etc.  The data for these searches also needs to be presented differently to the user than this “vendor” search.

There obviously exists a need to generically bind a repeating data structure to a repeating XML element in the InfoPath data source.

Better solution: Use a single repeating table with generic data structures

Disclaimer: I’m sure there are better ways to do this– specifically, the classes we discuss later could easily implement the IEnumerable interface, but for simplicity’s sake, I stuck with the out-of-the-box generic .NET classes using a “has-a” approach rather than an “is-a” approach.  I also realize that the overhead involved in using these IEnumerable objects may be too heavy for some more advanced users for whom memory management is a more stringent requirement.  The important thing here is the concept, not the implementation, so please feel free to throw together a more efficient implementation.

The RepeatingElement abstract class

This abstract class will define the repeating data that we use to fill repeating XML elements.  Every class that “is” a repeating element needs to have a Dictionary object whose keys represent the names of the fields of the repeating element, and whose values represent the values of those fields.  Below is the code for the abstract class– this will make more sense when we use it in a bit.  You will need to add a reference to the System.Collections.Generic assembly.

public abstract class RepeatingElement
{
    public Dictionary<string, string> DataFields;

    public RepeatingElement()
    {
        DataFields = new Dictionary<string, string>();
    }

}
This really isn’t necessary from a high-level coding perspective, especially since we only have one requirement for classes of this abstract type: they have a Dictionary<string, string> object called DataFields.  But we’ll use it anyway since it will help us to define the type of repeating data we want to use to fill a repeating XML element.

The Vendor class

For our original use case, the data we’re using to fill the repeating table is a collection of values uniquely identifying a vendor: its name, code, and address.  There are two additional fields that will be used to determine whether or not this vendor has been “selected,” and where in the repeating table this vendor exists.

I created a class called “Vendor” that inherits from the abstract “RepeatingElement” class.  The DataFields Dictionary<string, string> object will hold values that match the XML structure of the “VendorDataQuery” verbatim.  The class contains two constructors: one which defaults each value to a placeholder string, and another which allows the user to supply the value for each entry in the DataFields dictionary:

class Vendor:RepeatingElement
    {
        public Vendor()
        {
            DataFields = new Dictionary();
            DataFields.Add("VendorQueryDataName", "#NAME");
            DataFields.Add("VendorQueryDataCode", "#CODE");
            DataFields.Add("VendorQueryDataAddrLine1", "#ADDR1");
            DataFields.Add("VendorQueryDataAddrLine2", "#ADDR2");
            DataFields.Add("VendorQueryDataAddrLine3", "#ADDR3");
            DataFields.Add("VendorQueryDataAddrLine4", "#ADDR4");
            DataFields.Add("VendorQueryDataSelected", false.ToString());
            DataFields.Add("VendorQueryDataPosition", "1");
        }

        public Vendor(string name, string code, string addrLine1, string addrLine2, string addrLine3, string addrLine4, bool isChecked, int position)
        {
            DataFields = new Dictionary();
            DataFields.Add("VendorQueryDataName", name);
            DataFields.Add("VendorQueryDataCode", code);
            DataFields.Add("VendorQueryDataAddrLine1", addrLine1);
            DataFields.Add("VendorQueryDataAddrLine2", addrLine2);
            DataFields.Add("VendorQueryDataAddrLine3", addrLine3);
            DataFields.Add("VendorQueryDataAddrLine4", addrLine4);
            DataFields.Add("VendorQueryDataSelected", isChecked.ToString());
            DataFields.Add("VendorQueryDataPosition", position.ToString());
        }
    }

You can probably see where this is leading.  The DataFields member variable from one instance of the concrete Vendor class will be written to one instance of the repeating XML element, “VendorQueryData”.

Important note: All concrete classes that inherit from the RepeatingElement abstract class need to be structured the same way– the DataFields object of type Dictionary<string, string> needs to have keys corresponding to the names of the fields contained in the repeating XML element.

Now we need a way to group these Vendor objects in a structure that can be iterated and parsed out to individual instances of a repeating element.  I chose to use a List object; you can use your favorite IEnumerable data structure.

To summarize where we’re at now, we’ve created a simple abstract class called RepatingElement that has a Dictionary<string, string> member variable; we’ve created a class called Vendor, a RepeatingElement, whose DataFields member variable contains keys corresponding to the structure of the repeating element that we wish to fill.  We still need to write a function that will accept an enumerable collection of objects and write them to the XML repeating element.

The FillRepeatingTable method

Our last step is to fill the XML element with instances of the Vendor class.  Below is the function I wrote and placed in a static “Common” class.  The class iterates through each Dictionary<string, string> object in the List passed as a parameter and writes its contents to the supplied repeating XML element.  We could just as easily pass it a RepeatingElement object, but since we’re using the DataFields member variable and nothing else, I would just assume pass that.  I will demonstrate how to call this method below the method definition.

public static void FillRepeatingTable(DataSource mainDataSource, IXmlNamespaceResolver namespaceManager,
       string nameSpace, string xPathToGroupContainingRepeatingElement, string repeatingElementName, List<Dictionary<string, string>> repeatingData, int rowStart, int numberOfRows)
{
     //Create navigator on XmlDataSource
     XPathNavigator nav = mainDataSource.CreateNavigator();

     try
     {
         //For each of the rows specified, create an element in the repeating table
         for (int i = rowStart; i < rowStart + numberOfRows; i++)
         {
              //Create a temporary XmlDocument object
              XmlDocument doc = new XmlDocument();

              Dictionary row = repeatingData[i];

              //Create the repeating element within the temporary XmlDocument
              XmlNode repeatingElement = doc.CreateElement(repeatingElementName, namespaceManager.LookupNamespace(nameSpace));

              //Add a child ("column") to the repeatingElement ("row") for each KeyValuePair in the dictionary
              foreach (KeyValuePair column in row)
              {
                      XmlNode repeatingElementColumn = doc.CreateElement(column.Key, namespaceManager.LookupNamespace(nameSpace));

                      //Append the repeatingElementColumn node ("column") to the repeatingElement node ("row")
                      XmlNode nodeToAdd = repeatingElement.AppendChild(repeatingElementColumn);

                      //Parse strings representing boolean values to the lowercase variant
                      if (column.Value == "True" || column.Value == "False")
                          nodeToAdd.InnerText = column.Value.ToLower();
                      else
                          nodeToAdd.InnerText = column.Value;
              }

              //Append the repeatingElement node to the temporary XmlDocument
              doc.AppendChild(repeatingElement);

              //Append the document to the supplied XPath node
              mainDataSource.CreateNavigator().SelectSingleNode(xPathToGroupContainingRepeatingElement, namespaceManager).AppendChild(doc.DocumentElement.CreateNavigator());
         }
     }
     catch (System.ArgumentOutOfRangeException)
     {
         return; //Stop writing values to table if we reach the end of the dataset-- happens if user gives bad parameters
     }
   }
}

A few things to note regarding this function:

  • There are a lot of ways one could write this while maintaining the same functionality; this particular setup fit my needs
  • The basic structure mimics the same steps we took when writing data to a repeating element used by a repeating table, but has been generalized to accept any List of Dictionary<string, string> objects so that it is independent of the target repeating XML element structure
  • The repeating element MUST be appended to a containing element whose only child is the repeating element (see the “VendorQuery” element in the XML structure above)
Before I explain how to call this function, now seems like an appropriate time to showcase a goal for the final product (click for a larger image):
Vendor Search
Some things to notice:
  • Vendor addresses are displayed like real addresses, with up to four lines representing a single vendor
  • The vendor code and “VendorQueryDataSelected” check-box are on the bottom-right and top-right corner of each table cell, respectively
  • Most notably, there are three columns of data displayed in three separate repeating table controls; we’ll address this shortly…
To call the FillRepeatingTable method (partial implementation shown):

Create an empty List<Dictionary<string, string>> object:
List<Dictionary<string, string>> VendorQueryDataSet = new List<Dictionary<string,string>>();

For each row returned from the database query, create a new Vendor object and add its DataFields member variable to the list:

int position = 1;

 while(reader.Read())
 {
     numberOfResults++;

     Vendor vendor = new Vendor(reader["ADDR1"].ToString(),
            reader["VENDOR_CODE"].ToString(), reader["ADDR1"].ToString(),
            reader["ADDR2"].ToString(), reader["ADDR3"].ToString(),
            reader["ADDR4"].ToString(), position);

     //Add the Vendor object to the dataset
     VendorQueryDataSet.Add(vendor.DataFields);

     position++;
 }

Finally, call the FillRepeatingTable method, supplying the “VendorQueryDataSet” object as a parameter:

//Clear the Vendor repeating table (see method definition later in post)
ClearRepeatingTable(MainDataSource, NamespaceManager,
       "/my:InvoiceFields/my:Vendor/my:VendorQuery/my:VendorQueryData");

//Fill the Vendor repeating table
FillRepeatingTable(MainDataSource, NamespaceManager, "my",
       "/my:InvoiceFields/my:Vendor/my:VendorQuery", "VendorQueryData",
       VendorQueryDataSet, 0, VendorQueryDataSet.Count);

The ClearRepeatingTable method, which is called before our call to FillRepeatingTable, simply clears “old” data from the repeating table.  This is necessary for two reasons:

  1. If the user had previously searched for a vendor, we need to clear the old results before we can display the new results
  2. Even if this is the first time the repeating table has been filled, the default state for repeating XML elements in InfoPath is a single, empty row; we must remove this row before adding new XML elements because we are calling the AppendChild method on the repeating element node
The code for ClearRepeatingTable is below:
public static void ClearRepeatingTable(DataSource mainDataSource, IXmlNamespaceResolver namespaceManager, string xPathToRepeatingTable)
{
    XPathNavigator nav = mainDataSource.CreateNavigator();
    //Clear the repeating table items
    XPathNodeIterator iterator = nav.Select(xPathToRepeatingTable, namespaceManager);
    if (iterator.Count > 0)
    {
        for (int i = iterator.Count; i > 0; i--)
        {
            XPathNavigator reList = mainDataSource.CreateNavigator();
            XPathNavigator reListItems = reList.SelectSingleNode(xPathToRepeatingTable + "[" + i + "]", namespaceManager);
            reListItems.DeleteSelf();
        }
     }
}

Problems with repeating table controls

In our implementation thus far, we are filling a single repeating table with as many values as there are rows returned from our database query.  This is fantastic, with one small caveat: if we write more than a few rows to the repeating table, the table begins to expand the form beyond a reasonable size.  At this point, we have a few options for controlling the size of the repeating table as displayed to the user:

  1. Use InfoPath Filler instead of InfoPath Forms Services to display the form; this would allow us to place the repeating table inside a scrolling region
  2. Create two other data sources identical to our “VendorQueryData” repeating element and fill each repeating element with a select number of search results; for example, fill Repeating Element 1 with results #1-5, fill Repeating Element 2 with results #6-10, and fill Repeating Element 3 with results #11-15.  This would require writing an algorithm to re-fill each repeating element when the user clicks a button to “page through” the results; for example, click a “page right” button would then fill Repeating Element 1 with results #16-20 and so on…
  3. Place the same repeating element (“VendorQueryData”) on the form three times in the form of three repeating table controls and try to limit the rows shown in each table control by placing conditional formatting restrictions on each control

Option #1 is a no-go, as we already specified that this form must be browser-compatible (although if you are designing a form for InfoPath filler, I highly recommend utilizing scrolling regions).

Sadly, I poured blood, sweat and tears into Option #2 before finding a more elegant solution.  Option #2 is possible but requires tracking some global variables such as page count, current page, etc. and also requires much more overhead (each time the user executes a search, three repeating XML elements must be filled).

Option #3 does present its challenges in browser forms, but it does the trick.  I regret that I don’t remember the MSDN forum post where I learned that this was possible, but I will pass it along as I find it.  I adapted the poster’s suggestion for displaying a limited number of rows in a repeating table (and allowing the user to “page” through those results by creating a button that increments or decrements an index variable) and expanded the functionality to display multiple repeating table controls that read from a single repeating XML element.

The result is an elegant solution that allows the user to page through a single repeating table with an unlimited number of columns displayed at once.

For that, though, you’ll need to read the third (and final) installment of this post.  Stay tuned!

Advertisements

Managing Repeating Data with Code, Part 1: Use a ListBox Control

I have often encountered a very biting separation between repeating element controls in InfoPath, and code that manages the contents of these repeating elements.

The problem

Abstract: An InfoPath form presents the user with a search box and displays the results of the search in a meaningful way.  This is used for quick “lookups” while the user is filling out the form. 

We need a good way to manage the data returned from a search, as well as an elegant way to display this data to the user.

Example: Specifically, this search returns a list of “vendor” data to the user.  The user enters a search term, and the code-behind methods in the form execute a search by querying against a database.  Each row that is returned by the query needs to be presented to the user in a useful manner.  Each row has a vendor name, code, and a few lines of the vendor’s address for billing purposes.

InfoPath gives us a few ways to display repeating data: 

Decent solution: Use a listbox control

My first solution to this problem is the most straightforward because it involves simply writing values to the listbox:

Pros:

  • Easy to scroll through
  • Easy to fill using code

Cons:

  • Very difficult to display LOTS of data (i.e. the vendor’s full address)
  • No ability to add graphics
  • Data is all text; it’s pipe-delimited, which means we must parse it out again when the user selects it

In order to fit our use case requirements, I created a repeating element in the form’s data source called “Vendor_Listbox_Item” with one child field as follows:

Vendor_Listbox Data Structure

Manage the contents of the listbox using code

For each row returned from the database query, we’ll append an instance of this repeating element to the form’s data source.  The data will reside in the “Vendor_Listbox_Item_Name” field.

//Create an XmlDocument object
XmlDocument doc = new XmlDocument();

//Create Vendor_Listbox_Item element
XmlNode group = doc.CreateElement("Vendor_Listbox_Item", NamespaceManager.LookupNamespace("my"));

//Create Vendor_Listbox_Item_Name field, where we store the data returned from the database query
XmlNode field = doc.CreateElement("Vendor_Listbox_Item_Name", NamespaceManager.LookupNamespace("my")); //Listbox "value" source
XmlNode node = group.AppendChild(field);

//Parse the column values returned from the database row and append them to the data field
node.InnerText = reader["VENDOR_CODE"].ToString() + " | " + reader["ADDR1"].ToString()
    + " || \n" + reader["ADDR2"].ToString() + " ||| \n" + reader["ADDR3"].ToString() + " |||| \n" + reader["ADDR4"].ToString();

//Add the newly-created repeating element to the form's XML data source
MainDataSource.CreateNavigator().SelectSingleNode("//my:myFields/my:Vendor_Listbox", NamespaceManager).AppendChild(doc.DocumentElement.CreateNavigator());

An important note: Even though we could store more data in this repeating element by adding fields (for example, by adding a field to the repeating element for every address line), remember that an InfoPath listbox can only display ONE field.  That’s why we pipe-delimit the vendor address fields in order to display as much data to the user as possible.

Bind a listbox to a repeating XML element

We bind the repeating element to the listbox like this:

Vendor_Listbox Binding

When the user selects an item from the listbox, four different text box controls filter through the pipes to display each line of the vendor’s address.

The implementation of this “listbox” method is shown below, with actual vendor addresses blurred.  The listbox is on the left, and the four text box controls that break the vendor address into four lines is on the right:

Implementation of the search results listbox

Recap:

This method of binding search results to a repeating element and binding the repeating element to a listbox is simple and easy to accomplish.  However, the end product is messy.   Only by selecting a vendor from the listbox can the user display the vendor’s full address in an easy-to-read format.

Stay tuned for Part 2: Using a repeating table

InfoPath 2010 Validation in Repeating Tables – Check for a Duplicate of a Single Value

The problem:

I have a repeating table that is populated by code and contains a boolean value.  The boolean value represents whether or not the row is “selected.”  I want a user to be able to set ONLY ONE of the boolean values to true and thus “select” only one row; if more than one row is selected, a validation error should occur.  The highlighted field below is type boolean.

You’re probably thinking to yourself, isn’t that the purpose of radio buttons? Yes, it is: give the user multiple choices amongst which only one can be selected.  But there is no way to place a single radio button in a repeating table and allow it to be selected in only ONE row of the repeating table.  This is because the radio button can’t be bound to the repeating group.

The solution: Use an expression

Add validation to the VendorQueryDataSelected field using an expression.

Before I get into the specifics, I found this forum post where someone has a problem similar to mine, except they are attempting to weed out duplicate values for a single field within the repeating table.  I don’t care if there are duplicate values; in fact, if I implemented the method described there, the validation would throw an error because the default value of “false” for the boolean field is duplicated for every row in the table.

Instead, I only care if the value “true” is duplicated.  Here is the expression I used for the validation:

count(/my:InvoiceFields/my:Vendor/my:VendorQuery/

           my:VendorQueryData[my:VendorQueryDataSelected = “true”]) > 1

This reads IF (the COUNT OF (WHERE the value of the field VendorQueryDataSelected == “true”)) is greater than 1, then throw a validation error.

Or, generically:

count(/my:RootGroup/my:SubGroup/my:SubSubGroup[my:FieldToCheck = “true”]) > 1

Result: Below is my table with a single vendor selected:

Now, I select two vendors.  The validation event triggers:

Boom.