自定义 XPath 函数
使用自定义函数扩展 XPath 表达式。
引言
XPath 表达式是一个强大的工具,但是它也有一些局限性。有些你可能需要做的事情在规范中还没有提供,或者你可能需要做一些与规范略有不同的事情,或者根本没有办法做到,除非使用自定义代码。XPath 函数是提供额外功能来解决其他方法无法涵盖的问题的一种方式。
在本讨论中,我们将重点关注在 XPath 表达式中执行不区分大小写搜索的问题。创建和使用自定义函数和变量的技术当然可以应用于根据需要扩展你的 XPath 表达式来解决其他问题。
问题
XML 是一种区分大小写的语言,虽然这可能是件好事,但有时也会带来沮丧。验证 XML 可以确保格式正确,但有时这是不可能的,因为没有可用的模式,或者你无法控制 XML 格式,你只能接收你得到的 XML 并必须设法使其工作。当尝试使用 XPath 表达式选择节点时,“*//Address[@type = ‘home’]
” 和 “*//Address[@type = ‘Home’]
” 之间是有区别的。鉴于下面的 XML 片段,这两种查询只会返回一个节点。
<Addresses>
<Address type="Home">
<Street>100 Main St</Street>
<City>Uppercase</City>
<State>AZ</State>
<Zipcode>12345</Zipcode>
</Address>
<Address type="home">
<Street>100 Main St</Street>
<City>Lowercase</City>
<State>az</State>
<Zipcode>12345</Zipcode>
</Address>
<Address type="business">
<Street>1 Business Way</Street>
<City>Lowercase</City>
<State>AZ</State>
<Zipcode>12345</Zipcode>
</Address>
</Addresses>
但是,如果你需要查找所有节点,而不考虑大小写呢?一种进行此类匹配的方法是遍历节点列表并过滤结果,如下所示:
XPathDocument doc = new XPathDocument("Test.xml");
XPathNavigator nav = doc.CreateNavigator();
XPathNodeIterator nodes = nav.Select("*//Address");
foreach(XPathNavigator node in nodes)
{
string attr = node.GetAttribute("type", "");
if(attr.ToLower() == "home" )
{
// Do something...
}
}
这是低效且繁琐的,因为你必须检索节点,遍历它们,然后过滤掉不匹配的节点。更好的方法是首先过滤返回的结果。
XsltContext
NET Framework 支持通过指定 XsltContext
为 XPath 表达式添加自定义函数。这个抽象类为 XSLT 处理器提供了解析 XPath 表达式中使用的任何函数、变量和命名空间的环境。在从 XsltContext
派生类时,必须实现四个方法和一个属性。其中最重要的两个方法是 ResolveFunction
和 ResolveVariable
。尽管它们有目的和用途,但在我的探索中,我没有找到其余方法和属性的用法。
public override IXsltContextFunction ResolveFunction(string prefix,
string name, XPathResultType[] ArgTypes)
public override IXsltContextVariable ResolveVariable(string prefix, string name)
ResolveFunction
当使用包含函数的 XPath 表达式(如下所示)时,处理器必须能够在其上下文中解析该函数。
"*//Address[compare(string(@type),'home')]"
ResolveFunction
方法用于返回与传入的名称对应的 IXsltContextFunction
的实现。prefix 参数当然是可能与函数名称关联的命名空间前缀。ArgTypes
参数是函数中使用的每个参数的类型数组。在本例中,该函数将有两个类型为 string
的元素。
/// <SUMMARY>
/// Override for XsltContext method
/// </SUMMARY>
/// <param name="prefix">Namespace prefix for function</param>
/// <param name="name">Name of function</param>
/// <param name="ArgTypes">Array of XPathResultType</param>
/// <RETURNS>Implementation of IXsltContextFunction</RETURNS>
public override IXsltContextFunction ResolveFunction(string prefix,
string name, XPathResultType[] ArgTypes)
{
IXsltContextFunction func = null;
switch(name)
{
case "compare":
func = new CompareFunction();
break;
default:
break;
}
return func;
}
IXsltContextFunction
上面的 ResolveFunction
方法返回 IXsltContextFunction
的实现。这个接口有一个必须实现的方法和四个属性。我还没有发现属性被使用的情况,尽管 ReturnType
是为函数中的每个参数访问的。Invoke
方法是在处理 XPath 表达式期间被调用的。
public object Invoke(XsltContext xsltContext, object[] args,
XPathNavigator docContext)
{
if(args.Length != 2)
throw new ApplicationException("Two arguments must be
provided to compare function.");
string Arg1 = args[0].ToString();
string Arg2 = args[1].ToString();
if(String.Compare(Arg1, Arg2, true) == 0)
return true;
else
return false;
}
这个方法就是函数执行必要工作的地方。在这种情况下,在验证两个参数都可用后,会进行不区分大小写的比较。如果 string
匹配,该方法返回 true
,以便节点包含在 select 的节点集中。
XPathDocument doc = new XPathDocument("Test.xml");
XPathNavigator nav = doc.CreateNavigator();
// Create a custom context for the XPathExpression
CustomContext ctx = new CustomContext();
string XPath =string.Format("*//Address[compare(string(@type),'{0}')]", type);
// Create an XPathExpression
XPathExpression exp = nav.Compile(XPath);
// Set the context to resolve the function
// ResolveFunction is called at this point
exp.SetContext(ctx);
// Select nodes based on the XPathExpression
// IXsltContextFunction.Invoke is called for each
// node to filter the resulting nodeset
XPathNodeIterator nodes = nav.Select(exp);
ResolveVariable
如果你在 XPath 表达式中使用了变量,例如这样...
XPathExpression exp = nav.Compile("*//Address[compare(string(@type))=$value]");
...它将被调用 XsltContext
派生类的 ResolveVariable
方法来解析。
/// <SUMMARY>
/// Override for XsltContext method used to resolve variables
/// </SUMMARY>
/// <param name="prefix">Namespace prefix for variable</param>
/// <param name="name">Name of variable</param>
/// <RETURNS>CustomVariable</RETURNS>
public override IXsltContextVariable ResolveVariable(string prefix,
string name)
{
return new CustomVariable(name);
}
将返回一个实现 IXsltCustomVariable
的 CustomVariable
类。IXsltCustomVariable
接口只有一个方法需要实现。Evaluate
方法在运行时被调用以检索指定变量的值。
/// <SUMMARY>
/// Gets the value of the variable specified
/// </SUMMARY>
/// <param name="xsltContext">Context in which this variable is used</param>
/// <RETURNS>Value of the variable</RETURNS>
public object Evaluate(XsltContext xsltContext)
{
XsltArgumentList args = ((CustomContext)xsltContext).ArgumentList;
return args.GetParam(Name, "");
}
然后,该变量可以用于 IXsltContextFunction.Invoke
。
public object Invoke(XsltContext xsltContext, object[] args,
XPathNavigator docContext)
{
string Value =
((CustomContext)xsltContext).ArgumentList.GetParam("value", "").ToString();
string Arg1 = args[0].ToString();
if(String.Compare(Arg1, Value, true) == 0)
return true;
else
return false;
}
使用 XPath 表达式中的变量和不使用变量之间的区别在于调用 IXsltContextFunction.Invoke
方法的时机。使用变量时,此方法在迭代节点集时被调用;而不使用变量时,则在执行 Select
时被调用。
结论
本文演示了使用自定义 XPath 函数来解决一个简单的问题。你应该能够使用这些技术来扩展你的应用程序中的 XPath 功能。
历史
- 2006 年 8 月 28 日:初次发布