在 WinForms 应用程序中托管 InfoPath 2007






4.09/5 (6投票s)
在 WinForms 应用程序中托管 InfoPath 2007 的高级方面。

引言
InfoPath 是一个强大的模板编辑器,可用于创建和编辑模板。从 2007 版本开始,它也可以托管在 Windows Forms 应用程序中。如果您对此功能不熟悉,请参见:在自定义 Windows 窗体应用程序中托管 InfoPath 2007 表单编辑环境。本文旨在分享关于 InfoPath 2007 一个相当小众但文档记录很少的功能的经验,另一方面介绍一种将 InfoPath 2007 集成到 WinForms 应用程序中的替代方法。
评论:如果您在运行可执行文件时遇到问题,原因可能是您 1. 未安装 InfoPath 2007,2. 未安装 Office 的 .NET 功能(在 Office 2007 安装程序中)或 3. 需要重新保存 InfoPath 模板(右键单击 TravelRequest.xsn --> 设计 --> 保存并覆盖现有文件)。
背景
正如我稍后会更详细地指出的那样,将 InfoPath 2007 集成和托管到自定义 WinForms 应用程序中的主要缺点在于,将表单发布为“可安装表单模板”需要每次更改任何表单模板时都重新分发自定义应用程序。通常,创建和/或更改表单模板独立于自定义应用程序的开发 - 甚至可能应该让业务用户能够设计和上传这些表单模板。不幸的是,在我研究期间,我没有找到关于这种集成的好文档。
因此,我想展示一种集成 WinForms 应用程序和 InfoPath 2007 的另一种方法,该方法允许开发人员从任何类型的目标加载 InfoPath 2007 表单模板。此外,我希望分享一些相关的潜在问题,并提供一个粗略(但不完整)的功能概述,这可能有助于“概念验证”阶段。
当然,有很多不同的方法可以实现相同的灵活性(例如,InfoPath 和 Sharepoint)。然而,我认为在某些场景下,智能客户端仍然提供我们无法或不想仅通过 Web 技术实现的出色功能。
概述
此示例将加载、显示、编辑和更改 InfoPath 表单模板的视图。此外,我们需要一个解决方案,允许我们加载不同类型的模板,而无需重新安装整个应用程序。因此,它应该使我们能够加载发布在 Web 服务器上的表单模板等,并在 WinForms 应用程序中呈现此模板。这意味着此解决方案不需要将模板发布为“可安装表单模板”。此外,我将尝试分享一些关于部署、只读/编辑模式、部分受信任的表单以及以编程方式更改表单内容的经验。
创建 InfoPath 表单模板
首先,我们需要创建一个示例 InfoPath 2007 文档。我在附带的演示(项目:HostingInfopath2007)中包含了一个合适的文件。为了自己创建它,需要打开“Travel request”文档(InfoPath 2007 附带的 Microsoft 示例)。您的文档应如下所示。
之后需要做一些小的更改
1. InfoPath 文档的安全级别
需要更改文档的安全级别。如果您配置 InfoPath 自动检测适当的安全级别,并且您在域内工作且表单不访问外部资源,它将被设置为“域”。可以在“表单选项”(菜单“工具”-->“表单选项”-->“安全和信任”)对话框中调整安全级别。根据我的经验,我建议如果可能,将这些设置更改为“受限制”。原因如下:1. 软件的分布方式以及 2. 项目的安全设置。重要的是要认识到,使用 ClickOnce
部署的项目需要激活 CAS(代码访问安全)。这可以在 Windows Forms 项目的属性页的“安全”页面上完成。此外,我们还需要考虑到 InfoPath 托管环境中的 FormControl
类需要 CAS 设置为完全信任(FormControl 类)。然而,经过一些尝试,我得出了以下结论:
只要您不需要访问任何外部内容(文件、Web 服务等)或使用 VSTO 功能(表单的代码隐藏),您就可以在部分受信任的环境中运行您的表单(CAS 已激活)。如果您的表单能够在部分受信任的环境中运行,您就可以通过 ClickOnce 部署您的应用程序。
如果您使用 VSTO 或需要在 InfoPath 表单中访问任何外部内容,那么在部分受信任的环境中部署您的应用程序将非常困难甚至不可能。您应该会遇到很多困难,并考虑在不受限制的环境中部署您的应用程序(例如,通过安装程序,“不受限制”意味着任何 CAS 代码段都会被忽略,即与“完全信任”不同)。
评论:请注意,本示例中的表单并非作为表单模板部署。因此,由于托管环境内部处理方式不同,可能会出现一些不同的情况。

2. 离线 InfoPath 2007 表单
如果表单在 Web 环境中部署,InfoPath 可以在用户未连接(离线)的情况下让他们填写这些表单。这种功能不能在托管环境中直接使用。可能的解决方法是手动将表单保存在本地硬盘上,并从该位置在托管环境中打开它。在这种情况下,可能需要为本地版本设置不同的表单模板 ID。
我的经验是,在禁用 InfoPath 离线选项的情况下,InfoPath 表单的版本管理会更顺畅。因此,我们可以在 Windows Forms 应用程序中从 Web 服务器(模板需要先发布到那里)加载表单模板。如果我们用新版本覆盖已发布的模板,托管的 InfoPath 控件会自动识别此更改,并将旧数据(如果可能)合并到新模板中。如果我们激活模板的 InfoPath 离线功能,我们的表单环境将收到一个错误消息,表明 InfoPath 在您的本地计算机上找到了另一个版本。因此,它会在对话框中询问用户应该使用哪个版本。不幸的是,此对话框不会在窗体托管环境中出现,而只会在您在 InfoPath 独立版中加载表单时出现(例如,在浏览器中输入模板的 URL)。

3. 只读/编辑模式
在多个用户能够处理相同表单数据的集中式系统中,支持某种并发模式(通常是表单的悲观并发)是很正常的。话虽如此,在应用程序中支持只读和编辑模式似乎很简单。不幸的是,在我们的托管环境中,没有“开箱即用”的支持来将表单标记为只读。因此,需要一个小小的解决方法。可以切换不同的视图(甚至通过代码)。InfoPath 2007 的一项新功能允许将视图标记为只读。因此,可以创建一个新视图(任务窗格“视图”),复制(复制 - 粘贴)原始页面的内容,然后在“视图属性”对话框中将视图标记为只读。
评论:请注意,只读视图的命名应保持一致。


4. 发布
如前所述,在我看来,将 InfoPath 表单模板加载到 Forms 托管环境中存在两种不同的方法。首先,我们可以将其发布为“可安装表单模板”。此解决方案的影响是,每次更改模板后,都必须重新分发应用程序。其次,InfoPath 提供了将其发布到各种网络位置(Sharepoint、网络文件夹、Web 服务器)的选项,或者可以将其保存在本地硬盘上(在本示例中已完成)。无论模板发布在哪里,托管环境都可以读取它。
开发 InfoPath 托管环境
在接下来的部分,我将解释附件示例中的一些代码片段。
1. 创建新的表单模板(有或没有现有表单数据)
下面的代码示例打开另一个表单模板(有或没有合并现有表单数据)。因此,它可以用于创建新表单或加载现有表单数据。我想分享的第一个经验是关于在创建新表单之前先关闭表单。这样做是因为 FormControl 即使在之前未加载任何内容的情况下被关闭几次也没有关系。反过来,在 FormControl 已加载另一个表单的情况下创建/加载新表单,将抛出一个异常,提示它必须先关闭。
另一个有趣的方面是使用 IInitEventHandler
实例。在处理更改文档视图的详细信息时,我们将对此进行更深入的研究。
最后但并非最不重要的一点是,应该提到的是,可以使用 FormControl
类的 NewFromFormTemplate
方法加载任何表单模板。URL 参数可以指向任何类型的网络/本地源(包括 Web 引用 --> 例如 http:xxx)。此外,可以将包含表单数据的任何类型的流作为第二个参数传递,这些表单数据将与加载的模板合并。此功能可用于呈现/编辑特定模板的任何现有表单数据。如果您使用新版本的模板加载现有表单数据 - 假设您每次更新时都会覆盖旧模板 - 首先测试最终行为非常重要。
this.formControl.Close();
// Open / create a form
if (dataStream != null)
formControl.NewFromFormTemplate(
formUrlName.ToString(),
dataStream,
XmlFormOpenMode.Default);
else
formControl.NewFromFormTemplate(
formUrlName.ToString());
// Default view of InfoPath document could be the
// readonly or another one
this.infoPathInitEventHandler.InitReadOnlyFlag();
RefreshView(UIStatesForm.DocumentReadMode);
2. 以编程方式设置字段值
为了以编程方式设置字段值,需要弄清楚 XML 文档中表单数据的命名空间。此命名空间由 InfoPath 在后台创建,并标识一个唯一的表单模板。您也可以在 InfoPath 中找到此信息(菜单“文件”-->“属性”)。我在本示例中确定命名空间的方式是通过给定的前缀。因为在我所有的表单模板中(似乎是任何 InfoPath 表单模板的标准)前缀都是“my”,所以我使用以下方法来获取命名空间。
private static void FindInfoPathNamespace(
System.Xml.XPath.XPathNavigator navigator2,
out string namespaceName2,
out bool foundNamespace2)
{
namespaceName2 = string.Empty;
foundNamespace2 = false;
if (navigator2.MoveToFirstChild())
{
namespaceName2 = navigator2.LookupNamespace(@"xmlns:my");
while (namespaceName2 == null || namespaceName2.Length <= 0)
{
if (!navigator2.MoveToNext())
break;
namespaceName2 = navigator2.LookupNamespace(@"my");
};
}
if (namespaceName2 != null && namespaceName2.Length > 0)
foundNamespace2 = true;
}
因此,通过调用标准的 XPathNavigator 方法可以轻松更改值。更改文档中的值将立即更新 InfoPath 视图。
var navigator =
formControl1.XmlForm.MainDataSource.CreateNavigator();
string namespaceName;
bool foundNamespace;
FindInfopathNamespace(
navigator,
out namespaceName,
out foundNamespace);
var nav2 = navigator.Clone();
var found = nav2.MoveToFollowing(
fieldName,
namespaceName);
if (found)
nav2.SetValue(fieldValue);
3. 在 InfoPath 视图之间切换
在 FormControl
类的实现中,视图切换非常直接。可以通过 ViewInfos
属性读取有关所有可用视图的信息,并通过其 SwitchView
函数更改视图。捕获 SwitchView
函数经常抛出的 COMException 非常重要。当在短时间内多次切换视图时,可以重现此异常。在以下实现中,我假设如果未抛出异常,则视图已成功切换。
public void SwitchView()
{
TestInitialization();
try
{
ViewInfo readOnlyView = null;
ViewInfo editView = null;
foreach (ViewInfo view in this.formControl.XmlForm.ViewInfos)
{
if (string.Compare(view.Name.ToLower(
CultureInfo.CurrentCulture),
READONLY_VIEW_NAME,
StringComparison.CurrentCulture) == 0)
readOnlyView = view;
else editView = view;
}
var new_readonly = !this.readOnly;
if (new_readonly)
this.formControl.XmlForm.ViewInfos.SwitchView(readOnlyView);
else this.formControl.XmlForm.ViewInfos.SwitchView(editView);
// Switching view was successful
this.readOnly = new_readonly;
}
catch (COMException)
{
// switching view was not successfull, do nothing
}
}
或者,可以实现 IInitEventHandler
接口。此接口允许我们在视图切换或表单上下文更改时收到通知。为了使此功能正常工作,我创建了一个单独的类(在本示例中为嵌入式类),它实现了此接口。在 InitEventHandler
方法中,可以处理 InternalStartup
事件,在其中我们可以处理 ContextChanged
和/或 ViewSwitched
事件。不要尝试在构造函数中直接处理 ContextChanged
和/或 ViewSwitched
事件。这会抛出一个消息为“InfoPath 无法创建新空白表单”的异常。
此接口的实现应如下所示
internal class InfoPathInitEventHandler : IInitEventHandler
{
private InfoPathInitEventHandler() { }
FormControl formControl;
bool internalStartupDeclared = false;
private InfoPathInitEventHandler(
FormControl formControl)
{
this.formControl = formControl;
}
internal static InfoPathInitEventHandler CreateInstance(
FormControl formControl)
{
return new InfoPathInitEventHandler(
formControl);
}
#region IInitEventHandler Members
public void InitEventHandler(
object sender,
XmlForm xmlForm,
out Microsoft.Office.Interop.InfoPath.XdReadOnlyViewMode
viewsReadOnlyMode)
{
viewsReadOnlyMode =
Microsoft.Office.Interop.InfoPath.XdReadOnlyViewMode.xdDefault;
if (!internalStartupDeclared)
{
this.formControl.InternalStartup +=
new FormControl.EventHandler<EventArgs>(formControl_InternalStartup);
internalStartupDeclared = true;
}
}
void formControl_InternalStartup(object sender, EventArgs e)
{
this.formControl.EventManager.FormEvents.ContextChanged +=
new ContextChangedEventHandler(FormEvents_ContextChanged);
this.formControl.EventManager.FormEvents.ViewSwitched +=
new ViewSwitchedEventHandler(FormEvents_ViewSwitched);
}
#endregion
void FormEvents_ViewSwitched(object sender, ViewSwitchedEventArgs e)
{
}
void FormEvents_ContextChanged(
object sender,
ContextChangedEventArgs e)
{
}
}
最后,我们需要将此实现通知我们的 FormControl
实例。这可以在 FormControl
实例初始化后直接完成。
private void InitInfoPathInitEventhandler()
{
this.infoPathInitEventHandler =
InfoPathInitEventHandler.CreateInstance(
this.formControl);
this.formControl.SetInitEventHandler(
this.infoPathInitEventHandler);
}
我想再提一个小细节,关于更改视图。可以找出哪个视图是默认视图,即打开表单模板后显示的视图。以下代码片段检查默认视图是否命名为“readonly”。
private bool IsInitialReadOnly
{
get
{
return
(string.Compare(this.formControl.
XmlForm.ViewInfos.Default.Name,
READONLY_VIEW_NAME,
true,
CultureInfo.CurrentCulture) == 0);
}
}
4. InfoPath 表单模板验证
InfoPath 提供了非常强大的验证控件。可以将验证错误的数量和详细信息返回到托管应用程序。
public int ValidateInfoPath()
{
TestInitialization();
return this.formControl.XmlForm.Errors.Count;
}
5. 返回 InfoPath 表单数据
存储 InfoPath 数据的常规方法是通过将数据提交到不同的目标(Web 服务、电子邮件、托管环境)。这些选项都不为开发人员提供将数据直接存储在数据库中或在未满足所有验证要求的情况下存储表单数据的灵活性。幸运的是,表单数据可以手动从表单模板读取。此表单数据(字符串)可以手动存储,并在创建表单时(参见 1)流式化回表单模板。
public string FormData
{
[EnvironmentPermissionAttribute(
SecurityAction.LinkDemand)]
get
{
TestInitialization();
XmlForm xf = formControl.XmlForm;
if (xf != null) return xf.MainDataSource.
CreateNavigator().OuterXml;
else return string.Empty;
}
}
6. 使用 InfoPath 用户控件
最后,我们可以在应用程序中使用用户控件。这非常简单 - 因此我不再赘述。
static private void RefreshSwitchViewName(
InfoPathUserControl InfoPathUserCtrl,
Button buttonSwitchView)
{
buttonSwitchView.Text =
"Switch view (Readonly is " +
InfoPathUserCtrl.ReadOnly +
")";
}
static private void CreateDocument(
InfoPathUserControl InfoPathUserCtrl,
Button buttonSetFieldValue,
Button buttonSwitchView,
string InfoPathTemplate)
{
InfoPathUserCtrl.CreateFormTemplate(
new Uri(
Path.Combine(
Environment.CurrentDirectory,
InfoPathTemplate)));
buttonSetFieldValue.Enabled = true;
buttonSwitchView.Enabled = true;
RefreshSwitchViewName(
InfoPathUserCtrl,
buttonSwitchView);
}
static private void SetFieldValue(
InfoPathUserControl InfoPathUserControl)
{
InfoPathUserControl.SetFieldValue(
"name", Environment.UserName);
}
static private void SwitchView(
InfoPathUserControl InfoPathUserCtrl,
Button buttonSwitchView)
{
InfoPathUserCtrl.SwitchView(
!InfoPathUserCtrl.ReadOnly);
RefreshSwitchViewName(
InfoPathUserCtrl,
buttonSwitchView);
}
结论
在自定义 Windows Forms 应用程序中托管 InfoPath 2007 的能力为开发人员提供了一个工具来集成他们自己的强大模板驱动的表单编辑器。这在各种场景中都很有用。文档最完善的场景是将表单模板发布为可安装版本,但它缺乏在创建/版本管理表单模板和定制应用程序的版本管理之间的关注点分离。在大多数情况下,创建或编辑表单模板的频率会高于整个宿主应用程序。这个需求可以成功实现。
缺点:在创建、发布和验证表单等方面需要编写一些额外的自定义代码。
总而言之,在我过去参与的一个 n 层生产 WPF 应用程序中,它工作得相当好。
历史
刚刚发布了 1.0 版本