Create Dynamic InfoPath Forms Based on User Input

Posted by Bob Silva Mon, 29 Oct 2007 02:59:00 GMT

The Problem

To build a form that represents my companies timesheets which currently vary between 4 and 5 week long pay periods. So I need the user to be able to select the current pay period and based on a set of start and end dates, display a form that allows them to fill in their time for that period. Lets take a look at what we want to accomplish:

4-week Time Period

5-week Time Period

The Solution

Since InfoPath forms are simply displaying the underlying XML datasource using XSLT, by changing the XML datasource structure, we can change the representation of the form dynamically. In the image below, you can see that the design does not specify the individual weeks and days. Instead, I use a Repeating Section control to represent a Week and a Repeating Table to represent a Day. By cloning these nodes for each week and day in the pay period, we are able to generate our form. I'll let the code speak for itself on this one as it took me quite awhile to figure it out myself.




using Microsoft.Office.InfoPath;
using System;
using System.Xml;
using System.Xml.XPath;


namespace Timesheets___Contract
{
    public partial class FormCode
    {
        /// 
        /// Stores the initial schema to allow a quick reset
        /// when user changes time period.
        /// 
        private object InitialSchema
        {
             get { return FormState["InitialSchema"]; }
             set { FormState["InitialSchema"] = value; }
        }
        private object FirstLoad
        {
            get { return FormState["FirstLoad"]; }
            set { FormState["FirstLoad"] = value; }
        }
        private object SaveLocation
        {
            get { return FormState["SaveLocation"]; }
        }
        
        // NOTE: The following procedure is required by Microsoft Office InfoPath.
        // It can be modified using Microsoft Office InfoPath.
        public void InternalStartup()
        {
            EventManager.FormEvents.Loading += new LoadingEventHandler(FormEvents_Loading);
            EventManager.XmlEvents["/my:TimesheetData/my:period"].Changed += new XmlChangedEventHandler(period_Changed);
            EventManager.FormEvents.Submit += new SubmitEventHandler(FormEvents_Submit);            
        }

        public void FormEvents_Loading(object sender, LoadingEventArgs e)
        {
            // Grab the root of the Main Data Source
            XPathNavigator xpnRoot = this.MainDataSource.CreateNavigator();
            
            // Set some form state values
            FormState.Add("InitialSchema", xpnRoot.SelectSingleNode("/my:TimesheetData/my:weeks", NamespaceManager).InnerXml);
            FormState.Add("FirstLoad", false);

            // Determine the SaveLocation
            string strUri;
            if (Application.Environment.IsBrowser)
            {
                strUri = e.InputParameters["SaveLocation"].ToString();
            }
            else
            {
                strUri = this.Template.Uri.ToString();
            }
            FormState.Add("SaveLocation", strUri);

            // If this is the first time the form is opened, calculate the current period
            XPathNavigator xpnPeriod = xpnRoot.SelectSingleNode("/my:TimesheetData/my:period", NamespaceManager);
            if (string.IsNullOrEmpty(xpnPeriod.Value))
            {
                // Set a state variable to determine FirstLoad
                this.FirstLoad = true;
                this.CalculateAndSetCurrentPeriod(xpnRoot);
            }
        }

        /// <summary>
        /// Iterates the Time Periods from the SharePoint List
        /// Data Source and determines the current pay period.
        /// Sets the value of the pay period drop-down which fires
        /// the Changed event to build the form.
        /// </summary>
        /// <param name="root"></param>
        /// <param name="periodsRoot"></param>
        private void CalculateAndSetCurrentPeriod(XPathNavigator xpnRoot)
        {
            DateTime today = DateTime.Now;

            // Grab the root of the TimesheetPeriods Data Source
            XPathNavigator xpnTPRoot = DataSources["TimesheetPeriods"].CreateNavigator();

            // Loop the periods looking for the one this date falls in
            XPathNodeIterator xpniPeriods = xpnTPRoot.Select("/dfs:myFields/dfs:dataFields/dfs:Payroll_Periods", NamespaceManager);
            while (xpniPeriods.MoveNext())
            {
                DateTime dtStartingDate = DateTime.Parse(xpniPeriods.Current.SelectSingleNode("@Starting_Date", NamespaceManager).Value);
                DateTime dtEndingDate = DateTime.Parse(xpniPeriods.Current.SelectSingleNode("@Ending_Date", NamespaceManager).Value);

                if (today >= dtStartingDate && today <= dtEndingDate)
                {
                    // Set the Period dropdown to the current period
                    XPathNavigator xpnPeriod = xpnRoot.SelectSingleNode("/my:TimesheetData/my:period", NamespaceManager);
                    xpnPeriod.SetValue(xpniPeriods.Current.SelectSingleNode("@Payroll_Period", NamespaceManager).Value);
                    break;
                }
            }
        }

        /// <summary>
        /// Creates a DateTime array of values between 2 dates.
        /// </summary>
        /// <param name="startingDate"></param>
        /// <param name="endingDate"></param>
        /// <returns></returns>
        private DateTime[] FillDays(DateTime dtStartingDate, DateTime dtEndingDate)
        {
            TimeSpan tsDaySpan = dtEndingDate - dtStartingDate;
            // Add 1 day to span to accomodate for 00:00:00 time value.
            int iNumDays = System.Math.Abs(tsDaySpan.Days) + 1;
            DateTime[] dtRange = new DateTime[iNumDays];
            
            int i = 0;
            while (i < iNumDays)
            {
                dtRange[i] = dtStartingDate.AddDays(i);
                i++;
            }
            return dtRange;
        }

        private void SetNilValue(XPathNavigator xpnNode, string strValue)
        {
            if (xpnNode.MoveToAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance"))
            {
                xpnNode.DeleteSelf();
            }
            xpnNode.SetValue(strValue);
        }

        private void BuildTimesheet(DateTime dtStartingDate, DateTime dtEndingDate)
        {
            this.ResetTimesheet();

            // Calculate the date range
            DateTime[] dtRange = this.FillDays(dtStartingDate, dtEndingDate);

            XPathNavigator xpnRoot = this.MainDataSource.CreateNavigator();
            // Set some initial node elements
            XPathNavigator xpnWeeks = xpnRoot.SelectSingleNode("/my:TimesheetData/my:weeks", NamespaceManager);
            XPathNavigator xpnWeek = xpnWeeks.SelectSingleNode("my:week", NamespaceManager);
            XPathNavigator xpnDay;

            foreach (DateTime dtDay in dtRange)
            {
                string strDayOfWeek = ((int)dtDay.DayOfWeek + 1).ToString();
                xpnDay = xpnWeek.SelectSingleNode("my:days/my:day[" + strDayOfWeek + "]", NamespaceManager);
                if (xpnDay == null)
                {
                        XPathNavigator _xpnDays = xpnWeek.SelectSingleNode("my:days", NamespaceManager);
                        XPathNavigator _xpnDay = _xpnDays.SelectSingleNode("my:day[1]", NamespaceManager);
                        _xpnDays.AppendChild(_xpnDay);
                        xpnDay = xpnWeek.SelectSingleNode("my:days/my:day[" + strDayOfWeek + "]", NamespaceManager);
                }
                // Fill in the Date using "Jan 13" format
                XPathNavigator xpnDate = xpnDay.SelectSingleNode("my:date", NamespaceManager);
                this.SetNilValue(xpnDate, String.Format("{0} {1}", dtDay.ToString("MMM"), dtDay.ToString("%d")));
                XPathNavigator xpnDayOfWeekAttr = xpnDate.SelectSingleNode("@my:dayofweek", NamespaceManager);
                xpnDayOfWeekAttr.SetValue(strDayOfWeek);
                
                // New weeks start on Sunday (except for the last week)
                if (dtDay.DayOfWeek == DayOfWeek.Saturday && dtDay != dtEndingDate)
                {
                    XPathNavigator _xpnWeek = xpnWeeks.SelectSingleNode("my:week[1]", NamespaceManager);
                    xpnWeeks.AppendChild(_xpnWeek);
                    xpnWeek = xpnWeeks.SelectSingleNode("my:week[last()]", NamespaceManager);
                }
            }

        }

        /// <summary>
        /// Resets the timesheet to the default schema saved during form load.
        /// </summary>
        private void ResetTimesheet()
        {
            XPathNavigator xpnRoot = this.MainDataSource.CreateNavigator();
            xpnRoot.SelectSingleNode("/my:TimesheetData/my:weeks", NamespaceManager).InnerXml = this.InitialSchema.ToString();
        }

        /// <summary>
        /// If this is the first time the form is loaded, forces the timesheet
        /// to rebuild and redisplay when a new period is selected.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void period_Changed(object sender, XmlEventArgs e)
        {
            if ((bool)this.FirstLoad)
            {
                string strPeriod = e.Site.Value;
                XPathNavigator xpnTPRoot = this.DataSources["TimesheetPeriods"].CreateNavigator();
                XPathNavigator xpnPeriod = xpnTPRoot.SelectSingleNode("/dfs:myFields/dfs:dataFields/dfs:Payroll_Periods[@Payroll_Period=\"" + strPeriod + "\"]", NamespaceManager);
                DateTime dtStartingDate = DateTime.Parse(xpnPeriod.SelectSingleNode("@Starting_Date", NamespaceManager).Value);
                DateTime dtEndingDate = DateTime.Parse(xpnPeriod.SelectSingleNode("@Ending_Date", NamespaceManager).Value);

                this.BuildTimesheet(dtStartingDate, dtEndingDate);
            }
        }

        /// <summary>
        /// Submit the form to the SharePoint Timesheets library. Place the form
        /// in a folder equal to the name of the current period.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void FormEvents_Submit(object sender, SubmitEventArgs e)
        {
            // Get the XPN for the Period node
            XPathNavigator xpnPeriod = this.MainDataSource.CreateNavigator().SelectSingleNode("/my:TimesheetData/my:period", NamespaceManager);
            
            // Set the Submit Folder to the proper Period subfolder of the Form Library
            FileSubmitConnection fscMain = (FileSubmitConnection)this.DataConnections["Main submit"];
            fscMain.FolderUrl = this.SaveLocation + "/" + xpnPeriod.Value;

            // Execute the submit connection
            try
            {
                fscMain.Execute();
                e.CancelableArgs.Cancel = false;
            }
            catch (Exception ex)
            {
                e.CancelableArgs.Cancel = true;
            }
        }
    }
}

Comments

Leave a response

Comments