Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / automation

Auto Generating Properties for InfoPath 2007

0.00/5 (No votes)
8 Oct 2013CPOL6 min read 17.9K   172  
A way to add automatic properties to the FormCode for InfoPath 2007.

Updates

08/10/2013: I have spent a bit of time improving the library and added support for repeating elements (not groups yet). So it is now possible to set the value of a repeating element by index! Next update will be for groups :p

Introduction

InfoPath code behind unfortunately doesn't provide a simple and convenient way to access fields in the underlying data source. You are required to use XPath to get or set the values and NodeIterators etc. to loop through repeating nodes.

If you aren't careful this can lead to huge difficulties with maintenance in the future as you are effectively using "magic strings" no matter what you do.

The Old Way

So... I used to add a file called FormCode.Properties.cs to the solution

which contained a partial FormCode class.

I would then use const strings to store the XPath for each node and then add properties to get/set them.

I now have propeties available for getting setting each field in code behind and a central place to update the XPath strings. This makes use and maintenance much easier, however it is still a bit of a pain as every new field means adding another property.

All in all a better way to access fields than simply using MainDataSource.CreateNavigator().SelectSingleNode() to get or set values, however there are improvements to be made!

The New Way

I decided what I really wanted was to automatically generate the properties when the form template is built/run.

This turned out to be quite tricky as it goes. First off with 2007 we are stuck using VSTA 1.0 which is .net 2.0 and missing quite a lot of features you take for granted with newer IDE's and later C# versions.

My first idea was to use templates... nope :(

So i looked into things a bit more and ended up looking at Reflection.Emit and CodeDom. In the end I decided that it's pretty useless to generate code at Runtime because you want to be able to access it during development. But it's pretty hard to generate the code without the form running...

In the end my solution was as follows:

  1. I added a new class PropertyGenerator with a single static method Build(XmlFormHost form)
  2. Call PropertyGenerator.Build(this) from the InternalStartup() method of FormCode.
  3. Add the newly generated FormCode.Properties.cs file to the solution.
  4. Voila, I now have properties available in code :)

If I don't want to regenerate each time then I just comment out the call to Build()

Example Walkthrough

I will walk through the code a little later in the article but I suggest you download the example and try it yourself to see how it works.

 To use the download you can just extract the folder to "C:\" then right click "Properties Example.xsn" and click "Design". Then preview and you can test the results.

The Template

The design of the form is simple a bunch of fields (of each supported DataType) including groups and a repeating field.

The form has a box for each element and a repeating table.

The "Test Button" uses the code behind to set all the fields using the generated properties.

Image 1

The Code Behind

C#
using Microsoft.Office.InfoPath;
using System;
using System.Windows.Forms;
using System.Xml;
using System.Xml.XPath;
using mshtml;
using InfoPathHelpers;
using System.Collections.Generic;

 
namespace Properties_Example
{
    public partial class FormCode
    {
        public void InternalStartup()
        {
            ((ButtonEvent)EventManager.ControlEvents["TestButton"]).Clicked += 
                new ClickedEventHandler(TestButton_Clicked);
 
#if DEBUG
            Dictionary<string, Type> PropertyMapping = new Dictionary<string, Type>();
            PropertyMapping.Add("EnumField", typeof(Fruit));
            PropertyGenerator.Build(this, PropertyMapping);
#endif
        }

 
        public void TestButton_Clicked(object sender, ClickedEventArgs e)
        {
            StringField = "Hello, World";
            IntegerField = 25;
            BooleanField = true;
            DateTimeField = DateTime.Today;
            GroupAttribute = "Attribute";
            GroupField = "Group Element";
            EnumField = Fruit.Strawberry;


            for (int i = 1; i < 6; i++)
            {
                RepeatingNumbers[i] = i;
            }

            MessageBox.Show(EnumField.ToString());
        }

 
        public enum Fruit
        {
            Apple,
            Orange,
            Banana,
            Strawberry
        }
    }
}

The code behind is dead simple. I have added a reference to the library. Then called PropertyGenerator.Build() passing in a dictionary which is used to map specific fields e.g. EnumField to the Fruit enum.

The solution is then run  to generate the FormCode.Properties.cs file.

It is then possible to hook up the TestButton handler and get or set the properties using the appropriate CLR type.

Preview

When the form is previewed you should see something like this:

Image 2

And after the test button the following:

Image 3

Voila! A very easy way to set fields in InfoPath without messing around with the XPath.

The Library

Property Generator

The property generator is nothing special; but it is rather huge and unwieldy. It just takes a list of fields and uses them to write out the following for each field:

  • A private const string containing the XPath of the field.
  • A public property with the getter and setter both using methods contained in the library (TryGetValue and SetValue).

As I mentioned I used CodeDom for the code generation which is fairly straightforward.

The most interesting parts are how the indexed properties are created for repeating elements but it's not that tricky, more time consuming.

Get, Set, Go!

The set method is fairly straightforward:

C#
public static void SetValue<T>(XPathNavigator node, T value, bool isNillable)
{
    var Value = (object)value;
    var Type = typeof(T);
 
    if (value == null || string.IsNullOrEmpty(value.ToString()))
    {
        node.SetValue(string.Empty);
        if (isNillable)
        {
            SetNilAttribute(node);
        }
        return;
    }
 
    RemoveNilAttribute(node);
 
    if (Type == typeof(Boolean))
    {
        node.SetValue((bool)Value ? "1" : "0");
    }
    else if (Type == typeof(DateTime))
    {
        node.SetValue(((DateTime)Value).ToString("s", CultureInfo.CurrentCulture));
    }
    else if (Type.IsEnum)
    {
        node.SetValue(((int)Value).ToString());
    }
    else
    {
        node.SetValue(Value.ToString());
    }
}

Pass in the node you want to set the value on and the value and it will use the type to determine the format for the string. This is easy because all node values are strings in the end.

At present I am only handling basic ones but will continue to add more as time goes by. Any type not already handled will just yield a string property instead.

The TryGetValue method is a little trickier:

C#
public static bool TryGetValue<T>(XPathNavigator node, out T result)
{
    // If there is no node to get a value from then conversion fails automatically.
    // An exception is not thrown because this method provides checking via the returned value.
    if (node == null)
    {
        result = default(T);
        return false;
    }
 
    if (typeof(T) == typeof(string))
    {
        // Node values are stored as string so no conversion is needed. 
        // We still need to use ValueAs because this is a generic method.
        result = (T)node.ValueAs(typeof(T));
        return true;
    }
 
    if (typeof(T).IsEnum)
    {
        // Enum types do not have TryParse methods so if T is an enum then we
        // use Enum.Parse.
        // TODO: If re-targeting .net framework to version 4.0+ in future use Enum.TryParse.
        try
        {
            T enumValue = (T)Enum.Parse(typeof(T), node.Value);
            if (Enum.IsDefined(typeof(T), enumValue) || enumValue.ToString().Contains(","))
            {
                // The node value is defined in the enum so we can set result to the value
                // and return true as conversion succeeded.
                result = enumValue;
                return true;
            }
        }
        catch (ArgumentException) { }
 
        // If the value was not defined in the enum or there was an exception
        // conversion failed so set result to default and return false.
        result = default(T);
        return false;
    }
 
    MethodInfo TryParse = typeof(T).GetMethod("TryParse", new[] { typeof(string), typeof(T).MakeByRefType() });
 
    if (TryParse == null)
    {
        throw new InvalidOperationException("The supplied type cannot be parsed");
    }
 
    T Result = default(T);
    var Parameters = new object[] { node.Value, Result };
    bool Success = (bool)TryParse.Invoke(null, Parameters);
 
    if (Success)
    {
        Result = (T)Parameters[1];
    }
    result = Result;
    return Success;
}

Basically; if its a string we return a string easy! If it's an enum we parse the enum. And if it's anything else we try and parse it using the built in TryParse method.

Probably could be done better but it does the job for now :)

The Meaty Bit!

You didn't think it was as easy as that did you?

Well the hardest part it finding out what type each of the properties needs to be. To do this we really need to look at the schema. So I have written another set of helpers (under FormInfo) which can be used to get information about the nodes or open the schema in various ways.

Unfortunately it seems reading the schema is quite difficult as I was unable to use XmlSchemaSet due to the minOccurs and maxOccurs attributes always being 1! Instead I resorted to opening the schema as an XPathDocument (fast, readonly).

I noted a few things which are then assumed to be always correct:

  • The root element is always logically first in the schema.
  • Every node in the document has a corresponding node in the schema with a "ref" attribute, and one with a "name" attribute.
  • The InfoPath namespace is always "my", and the schema namespace is always "xsd".

It will be a lot of work to explain the entire method of walking the schema to get the required information but feel free to investigate the source yourself.

In the meantime it is enough to know that if you call FormInfo.GetAllNodeInfo(this) from the form code it hopefully returns a neat List<NodeInfo> with handy properties such as Name, XPath, IsRepeating, IsNillable, XmlType, InfoPathType.

These can then be used to determine how the properties should be created.

The Future...

  • Return XPathNavigator for Groups or Root?
  • I would like to provide indexed collections for repeating nodes which can be used as Property[x] to get the value of the node at position x. Update: This is done!
  • In the far future there is also the potential to create classes for complex nodes as well. With the ability to add remove them from the document in the code-behind.

Final Notes

I have tested this code quite a bit... however it is still very much a WIP! Use it at your own risk. I would suggest that it is very useful during development stage when rapidly changing form design is an issue. However once published it may be better to code changes by hand at this stage. It really depends on the complexity of your form and the requirments of the code behind...

Hope you enjoy looking at it :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)