InfoPath Master/Detail with SharePoint List Sources

I picked up some InfoPath requirements after one of our consultants had to leave so this one had to be done after dusting off the InfoPath brain cells.  I’m still not convinced this is the best way to get this done so please leave a comment if there’s a better way.

The InfoPath form needs to connect to two different SharePoint lists.  The first list is a list of Orders and the second is a list of Versions that apply to an Order.  There can be multiple Versions for each Order.  The goal was to create a master/detail type action where a user selects an order from a drop down, which then reloads the form with all of the version(s) and version details for that order displayed in a repeating table.  Doesn’t sound too hard, but I ran into some road blocks and had to resort to code.

Both roadblocks had to do with SharePoint lists as a source.  I don’t know if this is specific to SharePoint sources or just external sources in general, but you can’t use a SharePoint source in a master/detail relationship.  After dragging it onto the form, right click on the repeating table and view the properties.  The master/detail section is grayed out.  The second thing I ran into was trying to filter the SharePoint connection.  No luck there either, it’s not available.

After some searching, I found a post on implementing master/detail for a web based form which detailed some steps to pull in SharePoint data using a different method.  Instead of using the SharePoint List option when creating the connection, you use the XML option and point it to a URL which outputs the list data in XML format.  I won’t cover the details of that here as he covers it his post very well, but it’s using owssvr.dll.  That post solved my issues completely, but it was using code for InfoPath 2007.  I needed to convert that to be 2003 compatible. 

Since the 2003 compatible code is probably the only useful part of this post, here it is:

   1: public void OrderName_OnAfterChange(DataDOMEvent e)
   2: {
   3:     //Capture the new value for Order so we can filter the data just for that order
   4:     string newOrder = e.NewValue.ToString();
   5:  
   6:     //Get a reference to the Card Version Adapter
   7:     XMLFileAdapterObject cardVersion = thisXDocument.DataAdapters["Card Version owssvr"] as XMLFileAdapterObject;
   8:  
   9:     //Modify the Card Version adapter URL to append the filter
  10:     cardVersion.FileURL = cardVersion.FileURL + "&FilterField1=Order_x0020_Name&FilterValue1=" + newOrder;
  11:     
  12:     //Re-query
  13:     cardVersion.Query();
  14:  
  15:     if (e.IsUndoRedo)
  16:     {
  17:         // An undo or redo operation has occurred and the DOM is read-only.
  18:         return;
  19:     }
  20:  
  21:     // A field change has occurred and the DOM is writable.
  22:     // Write your code here.
  23: }

Pretty simple, but it took some SDK searching to find the right classes.  I’m using the XMLFileAdapterObject to grab the “Card Version owssvr” data connection, then I change the URL using FileURL similar to how Ishai modified the FileLocation property of the FileQueryConnection object.  Like I said, nothing fancy, but I thought it may come in handy for folks looking for a solution similar to Ishai’s, but who can’t use InfoPath 2007.

Test Twitter Notifier from Live Writer

Trying out the Twitter Notifier using Live Writer.  Found in the Live Writer SDK for Technical Preview.  This should show up on my Twitter feed.

FeedReader Update

Tim just put together a new release for his FeedReader web part which includes the code contributions I made a few months ago.  He details some installation steps on his blog here.

Thanks again for the opportunity to contribute, Tim!

Posted 30 June 08 10:21 by ryanm1201 | 0 Comments   
Filed under
Drill Here, Drill Now!

I guess if you sit and wait long enough, someone will do something to solve a problem.  In my eyes, the problem is our dependency on foreign oil.  There are two angles of attack to solve the problem.  First, reduce our dependency on oil regardless of source through alternative fuels.  This is a great idea and it's awesome to see work done in this area, including my uncle creating a home-brew hydrogen cell as an example.  Second, increase our production of oil so we don't need to depend on foreign sources.  If both of these could be done at the same time I think we'd be well on our way to solving the problem for good.

Not much is needed to increase our production.  The oil is there, we just need to go get it.  To that end, I came across a petition earlier which was started by Newt Gingrich called "Drill Here, Drill Now, Pay Less" which is meant to collect signatures which will be presented to the Senate.  He's already delivered 350,000, and the site is up to 805,000.  One of those signatures belonging to Chuck Norris!  Take a look at americansolutions.com and sign the petition if it looks like a good solution to you!

Posted 16 June 08 07:36 by ryanm1201 | 0 Comments   
Filed under
FeatureReceiver For Applying Custom Web.Config Changes

As I was reading through my SharePoint feeds I came across a post that struck a cord with me.  The post is by Mike Stringfellow and is about updating sections of the web.config through a SPFeatureReceiver.  It struck a cord because I had developed something very similar last July to make it simpler to make changes to the web.config through deployment.  It was always on my TODO list to blog about it but I obviously never got around to it.  Thanks to the "reminder" from Mike, let's see what I did!

(By the way, the purpose of this is not to steal any of Mike's thunder.  Actually, it looks like his solution is built better than mine, so I'll give credit where credit is due.  My hope is to provide some nuggets to the community with the hope that a public solution can be made available.  Mike, if you read this, perhaps we could put together a CodePlex project?)

I'll point out two things worth mentioning, then I'll let the code speak for itself as once you understand these two concepts the rest falls into place.

First is the use of the SPWebConfigModification class to manage the updates to the web.config file.  The class is flexible...it allows you to create any modification, then using xpath define where the modification lives in web.config.  So with a little string manipulation to create the entry and some xpath work to find the location in the config file, all that's left is to determine if we're performing an Add or Remove and then call the proper method on the SPWebApplication.WebConfigModifications.  You'll see this illustrated in the UpdateWebConfig method in the code below.

Second is the use, and format, of a settings file which needs to be packaged with the Feature and defines the changes to be made to web.config.  This allows the developer to simply include a new settings file in their Feature and the FeatureReceiver pulls that in and reads the changes.  No code for the developer.  As you can see in the code, the file needs to be named WebConfigChanges.xml.  Here's the XSD:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="WebConfigChanges" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="WebConfigChanges" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="WebConfigChange">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Attributes" minOccurs="0" maxOccurs="1">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded">
                      <xs:complexType>
                        <xs:attribute name="Name" type="xs:string" />
                        <xs:attribute name="Value" type="xs:string" />
                      </xs:complexType>
                    </xs:element>
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name="XPathLocation" type="xs:string" />
            <xs:attribute name="ElementName" type="xs:string" />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>
 
Here's some explanation on the file:
 
Name Description
WebConfigChanges A collection of WebConfigChange entries to be applied to the web.config file. The top element of the file.
WebConfigChange A single WebConfigChange to be applied to the web.config file.
XPathLocation Gives the XPath location to the element where the new entry should be inserted.
ElementName The name of the element to insert.
Attributes A collection of Attribute entries to be applied to the element being inserted.
Attribute A single Attribute to be applied to the element being inserted.
Attribute:Name The name of the attribute.
Attribute:Value The value of the attribute.
 
And an example which enters a new authorizedType entry for a new custom workflow activity:
 
<?xml version="1.0" encoding="utf-8" ?>
<WebConfigChanges>
  <WebConfigChange XPathLocation="configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes"
                     ElementName="authorizedType">
    <Attributes>
      <Attribute Name="Assembly" Value="Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4" />
      <Attribute Name="Namespace" Value="Company.Moss.Activities" />
      <Attribute Name="TypeName" Value="*" />
      <Attribute Name="Authorized" Value="True" />
    </Attributes>
  </WebConfigChange>
</WebConfigChanges>

Hopefully that all makes sense.  That foundation being set, I'll let the code explain the rest.  If you have any questions or suggestions please leave a comment.

using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
 
namespace Company.Moss.FeatureReceiver
{
    class CustomFeatureReceiver : SPFeatureReceiver
    {
        protected SPFeatureReceiverProperties _properties;
 
        #region base overrides
        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
            //throw new Exception("The method or operation is not implemented.");
        }
 
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {
            //throw new Exception("The method or operation is not implemented.");
        }
 
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            //This is where the WebConfigChanges file should be
            string fileLoc = properties.Definition.RootDirectory + "\\WebConfigChanges.xml";
 
            //Check to see if a WebConfigChanges.xml file exists. If yes, we have work to do
            if (System.IO.File.Exists(fileLoc))
            {
                //Grab the properties
                _properties = properties;
 
                this.ProcessChanges(fileLoc, false);
            }
        }
 
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            //This is where the WebConfigChanges file should be
            string fileLoc = properties.Definition.RootDirectory + "\\WebConfigChanges.xml";
 
            //Check to see if a WebConfigChanges.xml file exists. If yes, we have work to do
            if (System.IO.File.Exists(fileLoc))
            {
                //Grab the properties
                _properties = properties;
 
                this.ProcessChanges(fileLoc, true);
            }
        }
        #endregion
 
        private void ProcessChanges(string FileLocation, bool removeModification)
        {
            string xPathLocation;
            string elementName;
            Dictionary<string, string> attributes = new Dictionary<string, string>();
 
            using (XmlReader reader = XmlReader.Create(FileLocation))
            {
                //Loop through all of the changes
                while(reader.ReadToFollowing("WebConfigChange"))
                {
                    //Clean out any attributes from past iterations
                    attributes.Clear();
 
                    xPathLocation = reader.GetAttribute("XPathLocation");
                    elementName = reader.GetAttribute("ElementName");
                    
                    //Make sure we have at least a path and element
                    if (xPathLocation == null || elementName == null)
                        throw new Exception("WebConfigChange missing required XPathLocation or ElementName attributes");
 
                    //Get the Attributes to apply
                    if (reader.ReadToDescendant("Attribute"))
                    {
                        do
                        {
                            attributes.Add(reader.GetAttribute("Name"), reader.GetAttribute("Value"));
                        } while (reader.ReadToNextSibling("Attribute"));
                    }
 
                    //Do the update
                    UpdateWebConfig(xPathLocation, elementName, attributes, removeModification);
                } 
            }
        }
        
        private void UpdateWebConfig(string XPathLocation, string ElementName,  
            Dictionary<string, string> Attributes, bool removeModification)
        {
            try
            {
                SPWebApplication webApp = null;
 
                //Get the web app
                //First check if it was deployed to a Site Collection
                SPSiteCollection siteCol = _properties.Feature.Parent as SPSiteCollection;
                if (siteCol == null)
                {
                    //Check if it was deployed to a site
                    SPSite site = _properties.Feature.Parent as SPSite;
                    if (site == null)
                    {
                        //Check if it was deployed to a Site
                        SPWeb web = _properties.Feature.Parent as SPWeb;
                        if (web != null)
                            webApp = web.Site.WebApplication;
                    }
                    else
                        webApp = SPWebApplication.Lookup(new Uri(site.Url));
                }
                else
                    webApp = siteCol.WebApplication;
 
                if (webApp != null)
                {
                    SPWebConfigModification modification =
                        new SPWebConfigModification(ElementName + CreateAttributeString(Attributes), XPathLocation);
                        
                    modification.Owner = "Company.Moss.FeatureReceiver.CustomFeatureReceiver";
                    modification.Sequence = 0;
                    modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
                    modification.Value =
                         string.Format(CultureInfo.InvariantCulture,
                         CreateModificationValueString(ElementName, Attributes),
                         CreateModificationValueArgs(Attributes));
 
                    if (removeModification)
                        webApp.WebConfigModifications.Remove(modification);
                    else
                        webApp.WebConfigModifications.Add(modification);
 
                    SPFarm.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
                }
                else
                    throw new ApplicationException("Could not locate a web application");
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = "WebConfigFeature";
                el.WriteEntry(ex.Message);
            }
        }
 
        /// <summary>
        /// Accepts a dictionary object with all of the attributes for the web modification and
        /// creates a string representing the attribute values which can be used when creating
        /// the SPWebConfigModification object.
        /// </summary>
        /// <param name="Attributes"></param>
        /// <returns></returns>
        private string CreateAttributeString(Dictionary<string, string> Attributes)
        {
            //Create a string that looks like this (no line breaks):
            //[@Assembly=\"Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4\"]
            //[@Namespace=\"Company.Moss.Activities\"]
            //[@TypeName=\"*\"][@Authorized=\"True\"]
 
            string result = "";
 
            //Check if there are attributes
            if (Attributes.Count > 0)
            {
                foreach (KeyValuePair<string, string> kvp in Attributes)
                {
                    result += "[@" + kvp.Key + "=\"" + kvp.Value + "\"]";
                }
            }
            
            return result;
        }
 
        private string