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:
- I added a new class
PropertyGenerator
with a single static method Build(XmlFormHost form)
- Call
PropertyGenerator.Build(this)
from the InternalStartup()
method of FormCode. - Add the newly generated FormCode.Properties.cs file to the solution.
- 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.
The Code Behind
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:
And after the test button the following:
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:
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:
public static bool TryGetValue<T>(XPathNavigator node, out T result)
{
if (node == null)
{
result = default(T);
return false;
}
if (typeof(T) == typeof(string))
{
result = (T)node.ValueAs(typeof(T));
return true;
}
if (typeof(T).IsEnum)
{
try
{
T enumValue = (T)Enum.Parse(typeof(T), node.Value);
if (Enum.IsDefined(typeof(T), enumValue) || enumValue.ToString().Contains(","))
{
result = enumValue;
return true;
}
}
catch (ArgumentException) { }
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 :)