65.9K
CodeProject 正在变化。 阅读更多。
Home

XAML 中的参数化 XPath

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2014年12月23日

CPOL

8分钟阅读

viewsIcon

13847

downloadIcon

131

WPF/XAML 中参数化 XPath 的实现。

在此处 下载 XPathy 源代码

背景

有关 XPath 语法快速概述,请参阅: https://w3schools.org.cn/xpath/xpath_syntax.asp 。

引言

WPF 数据绑定框架不支持参数化 XPath,而我需要一个解决方案。 做出了一些妥协,但我很高兴拥有此功能,并欢迎您提供改进建议。

由于我有一个数据驱动的应用程序,我的任务要求我从半持久化的 XML 文件(例如产品目录)构建 UI,同时将用户的选择保存在另一个文件中。 没有参数化 XPath,这很难实现。

产品目录   UI   保存状态
<Deployments>
   <Deployment name="Product Alpha">
      <SystemRole name="vSphere Host" />
      <SystemRole name="Database Server" />
      <SystemRole name="Data Server" />
      <SystemRole name="Telephony Server" />
      <SystemRole name="Media Server" />
   </Deployment>
   <Deployment name="Product Beta">
      <SystemRole name="MS Terminal Server" />
      <SystemRole name="Database Server" />
      <SystemRole name="IIS Web Server" />
      <SystemRole name="Media Server" />
   </Deployment>
</Deployments>
>> >>
<SiteSurvey Package="Product Alpha">
   <System IPDN="mysql.sitedomain.com">
      <SystemRole name="vSphere Host" assigned="0" />
      <SystemRole name="Database Server" assigned="true" />
      <SystemRole name="Data Server" assigned="0" />
      <SystemRole name="Telephony Server" assigned="0" />
      <SystemRole name="Media Server" assigned="0" />
   </System>
   <System IPDN="esx.sitedomain.com">
      <SystemRole name="vSphere Host" assigned="true" />
      <SystemRole name="Database Server" assigned="0" />
      <SystemRole name="Data Server" assigned="0" />
      <SystemRole name="Telephony Server" assigned="0" />
      <SystemRole name="Media Server" assigned="0" />
   </System>
</SiteSurvey>

第一步 (命名空间)

将源模块添加到您的项目中,或将其构建为独立的程序集并添加到窗口的命名空间中。

<Window x:Class="CONTROL.SiteSurvey"
   ...
   xmlns:xpy="clr-namespace:XPathy;assembly=XPathy"
   ...
>

第二步 (资源)

添加 XPathy 转换器以及用于读取和写入 XML 数据以驱动 XAML UI 的各种 XmlDataProviders。 除了 IMultiValueConverter,我还展示了两个 XmlDataProviders,一个用于产品目录,一个用于存储的用户选择。

<Window.Resources>
   <xpy:ParameterizedXPathConverter x:Key="XPathConverter" />
   <local:XDeploymentDataProvider x:Key="DeploymentXML" />
   <local:XSessionDataProvider x:Key="SiteXML" />
</Window.Resources>

第三步 (XAML)

第三步之一点 (简单 XPath)

我需要一个 ComboBox 让用户从目录中选择产品。 这种 XPath 是完全支持的,并且在许多应用程序中都很常见。

<ComboBox x:Name="_Package"
   IsSynchronizedWithCurrentItem="True"
   ItemsSource="{Binding Source={StaticResource DeploymentXML}, XPath=Deployments/Deployment, Mode=OneTime}"
   DisplayMemberPath="@name"
   SelectedValuePath="@name" />

第三步之二 (具有运行时参数替换的 OneWay XPath)

然后,我需要一个 ListBox,其中包含从 ComboBox 指定的产品目录部分读取的产品选项。 这就是我需要在 XAML 中使用参数化 XPath 来适应运行时读取的数据,以响应用户的选择,并随后更改 ListBox 的内容。

<GroupBox Header="System Role(s)">
   <ListBox>
      <ListBox.ItemsSource>
         <MultiBinding Converter="{StaticResource XPathConverter}" ConverterParameter="Deployments/Deployment[@name=&quot;{1}&quot;]/SystemRole/@name" Mode="OneWay">
            <Binding Source="{StaticResource DeploymentXML}" Mode="OneWay" />
            <Binding ElementName="_Package" Path="SelectedValue" Mode="OneWay"/>
         </MultiBinding>
      </ListBox.ItemsSource>

这引入了复杂性的第一个阶段,因为它需要参数化 XPath,但只需要一个 "OneWay" 绑定。 ConverterParameter 用于指定参数化 XPath。 第一个 Binding 作为 XML 源,后面的 Bindings 作为参数值。 我使用基于一的参数化索引,因为 XPath 规范在其谓词中使用基于一的索引。 我还将参数替换格式模仿 C# 使用大括号的模型(例如“{1}”)。 HTML 实体 &quot; 必须用于在 XPath 中插入引号字符,因为 XAML 读取器在处理文字引号或转义引号时会混淆。

在转换器中,XPath 将被展开为类似 Deployments/Deployment[@name="Product Alpha"]/SystemRole/@name 的内容,它会返回产品目录“Product Alpha”部分中的所有 SystemRole 名称。 当用户将 ComboBox 的选择更改为“Product Beta”时,update-target-binding 将重新运行转换器,生成更新的 XPath 和来自备选选择(例如,Deployments/Deployment[@name="Product Beta"]/SystemRole/@name)的更新的 ListBox。

第三步之三 (具有运行时参数替换的 TwoWay XPath)

最后,我需要保存用户的选择,这需要一个 "TwoWay" 转换器和一系列技巧、妥协、限制和变通方法来实现此功能。 这是 ListBox 控件的其余 XAML。

      <ListBox.ItemTemplate>
         <DataTemplate>
            <StackPanel>
               <CheckBox Content="{Binding Path=Item.Value, Mode=OneTime}" >
                  <CheckBox.Resources>
                     <xpy:ParameterizedXPathConverter x:Key="lXPathConverter"/>
                  </CheckBox.Resources>
                  <CheckBox.IsChecked>
                     <MultiBinding Converter="{StaticResource lXPathConverter}" Mode="TwoWay" ConverterParameter="/site/SiteSurvey/System[@IPDN=&quot;{1}&quot;]/SystemRole[@name=&quot;{2}&quot;]/@assigned?Key=cbXPath&DefaultValue=0" >
                        <Binding Source="{StaticResource SiteXML}" Mode="OneWay" />
                        <Binding ElementName="_SystemNode" Path="Text" Mode="OneWay" />
                        <Binding Path="Item.Value" Mode="OneWay"/>
                     </MultiBinding>
                  </CheckBox.IsChecked>
               </CheckBox>
            </StackPanel>
         </DataTemplate>
      </ListBox.ItemTemplate>
   </ListBox>
</GroupBox>

将转换器声明为 CheckBox 控件的资源,位于列表的 ItemTemplate 中,这是一个技巧用于为每个 CheckBox 提供其自己的内存中转换器实例。 在 "TwoWay" 绑定中,Converter 的 ConvertBack() 方法会调用目标控件的值;期望为每个绑定的源返回一个值数组。 但是,根据输入和预期输出,无法确定哪个 ListBoxItem 被指定用于接收更新。 我能找到的最优雅的妥协是保留派生 XPath 的副本与转换器实例一起,并使用此保存的 XPath 来使用目标值更新源 XmlDataProvider。 因此,作为变通方法"TwoWay" 在转换器中实现。 从扩展的 XPath 到返回值的源绑定没有任何连接……

为了便于妥协 保留实例数据,我遵循了 URL 参数的规范,并实现了将名称-值对附加到 XPath 末尾的转换器参数的支持。 问号字符('?')分隔 XPath 的结束和转换器参数或选项的开始,每个参数或选项用与号('&')分隔。 像引号字符一样,必须使用 HTML 实体 "&" 来表示与号,以免混淆 XAML 编译器。 这种变通方法 使用 URL 参数语法来指导转换器行为的方式似乎是合适的,因为 XPath 规范中既不使用问号也不使用与号。

当转换器在窗口中的不同控件之间使用时,窗口的静态资源就足够了,因为它可以区分不同的控件实例,方法是使用不同的“Key”选项值。 在我的 CheckBoxes 列表中的动态创建的控件的情况下,每个 CheckBox 都必须使用自己的转换器实例。 因此,即使每个 CheckBox 的“Key”值相同,它们也分别解析到自己保存的 XPath 的单独实例。 “Key”参数是必需的,以方便在 MultiBinding 元素上指定的 Mode="TwoWay" 绑定。

您在此处看到 XPath 有两个替换参数。 参数和替换的数量只有外部实际限制。 只需将大括号占位符添加到 XPath 字符串以及相应的 Bindings 作为替换参数即可,根据需要添加。 但是当前实现中有XPath 表达式限制。 XmlDataProvider 类不直接支持创建元素或属性;因为它主要是一个读取器。 因此,要设置由 XPath 指定的值(或 InnerText),元素和属性必须首先完全存在。 作为变通方法,当指定“DefaultValue”参数时,XPath 的元素和属性将在源 XmlDataProvider 中创建,完整路径设置为指定的“DefaultValue提供,前提是 XPath 中的每一步都是一元且无歧义的。 “DefaultValue”参数通常是必需的,以方便在 MultiBinding 元素上指定的 Mode="TwoWay" 绑定,尤其是在源 XML 为空时。 我的意图不是编写一个完整的 XPath 解析器,而是支持参数化替换,以识别单个 XML 节点。 因此,这种变通方法 对于 "TwoWay" 绑定,不支持使用函数、通配符或非等式比较的 XPath,并且会倾向于在源 XML 中创建节点。 换句话说,只要 XPath 中的每一步都从源 XmlDataProvider 返回零个或一个节点,"TwoWay" 绑定就会成功,并留下痕迹。

该实现还有一个限制,即不支持 Mode="OneWayToSource" 绑定,因为 XPath 必须在目标值转换期间展开,然后才能将目标值推送到源。

内部一瞥

public class ParameterizedXPathConverter : IMultiValueConverter
{
   private Dictionary<string, Tuple<object, string>> pxp_source_xpathf_dic = null;

   private static int IndexOfOptions(string parameter)
   {
      char quotechar = '\0';

      for (int i = 0; i < parameter.Length; ++i )
      {
         switch (parameter[i])
         {
            case '?':
               if (quotechar == '\0')
                  return i;
               break;

            case '\'':
            case '"':
               if (quotechar == '\0')
                  quotechar = parameter[i];

               else if (quotechar == parameter[i])
                  quotechar = '\0';

               break;
         }
      }
      return -1;
   }

使用 Dictionary 类来跟踪“Key”索引的 XmlDataProviders 及其展开的 XPath 字符串。 另一个静态函数用于查找未加引号的问号字符,以分隔 XPath 的结束和转换器行为选项的开始。

   public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
   {
      string xpathf = (parameter as string).Replace("&quote;", "\"");
      for (int i = 1; i < values.Count(); i++)
         xpathf = xpathf.Replace("{" + i.ToString().Trim() + "}", values[i] as string ?? "");

      int indx = IndexOfOptions(xpathf);
      string[] options = (indx == -1) ? null : xpathf.Substring(indx + 1).Split(new char[] { '&' });

      if (indx > -1) xpathf = xpathf.Substring(0, indx);

      Dictionary<string, string> option_dic = null;

      if (indx > -1)
      {
         option_dic = new Dictionary<string,string>();
         foreach (string opt in options)
         {
            string[] pair = opt.Split(new char[] { '=' }, 2);
            if (pair.Count() == 2)
            {
               if (char.IsPunctuation(pair[1][0]))
                  pair[1] = pair[1].Trim(pair[1][0]);

               option_dic.Add(pair[0].ToLower(), pair[1]);
            }

            else if (pair.Count() == 1)
               option_dic.Add(pair[0].ToLower(), null);
         }
      }

      if (pxp_source_xpathf_dic == null)
         pxp_source_xpathf_dic = new Dictionary<string, Tuple<object, string>>();

      if (option_dic != null && option_dic.ContainsKey("key"))
      {
         if (pxp_source_xpathf_dic.ContainsKey(option_dic["key"]))
            pxp_source_xpathf_dic.Remove(option_dic["key"]);

         pxp_source_xpathf_dic.Add(option_dic["key"], new Tuple<object, string>(values[0], xpathf));
      }

      string defaultvalue = (option_dic != null && option_dic.ContainsKey("defaultvalue")) ? option_dic["defaultvalue"] : null;

      return XPathy.TapXPath(values[0], xpathf, defaultvalue, targetType);
   }

最初,我在 HTML 实体“&quot;”方面遇到了麻烦。 随着实现从内联 XAML 属性转移到 MultiBinding 元素,这种非标准的替换不再是严格必需的。 但我已将其保留在 Convert() 函数的开头部分,作为对先前麻烦的不恰当提醒。

Convert() 函数的第一部分执行所有替换。 循环从 values 数组的第二个元素开始,因为第一个元素是 XmlDataProvider。 替换完成后,将提取转换器选项(如果存在)。 如果需要,将创建实例字典,并根据“Key”选项的指示保存 XmlDataProvider 和展开的 XPath 源。 如果存在,将提取“DefaultValue”选项。

可以推断出替换可以应用于转换器参数的选项区域,但不应将其应用于“Key”选项,因为 ConvertBack() 函数没有替换输入,并且将无法稍后将未展开的“Key”与已缓存的展开形式进行匹配。

最后,如果存在“DefaultValue”并且路径中的每一步都可以一元解析,则调用 TapXPath() 来尝试在 XmlDataProvider 源中创建完全解析的 XPath。 TapXPath() 调用返回的值将返回给绑定层,用作更新的目标值。

   public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
   {
      List<object> l = new List<object>();

      int indx = IndexOfOptions(parameter as string);
      string[] options = (indx == -1) ? null : (parameter as string).Substring(indx + 1).Split(new char[] { '&' });
      Dictionary<string, string> option_dic = null;

      if (indx > -1)
      {
         option_dic = new Dictionary<string, string>();
         foreach (string o in options)
         {
            string[] pair = o.Split(new char[] { '=' }, 2);
            if (pair.Count() == 2)
            {
               if (char.IsPunctuation(pair[1][0]))
                  pair[1] = pair[1].Trim(pair[1][0]);

               option_dic.Add(pair[0].ToLower(), pair[1]);
            }

            else if (pair.Count() == 1)
               option_dic.Add(pair[0].ToLower(), null);
         }
      }

      if (option_dic != null && option_dic.ContainsKey("key") && pxp_source_xpathf_dic.ContainsKey(option_dic["key"]))
      {
         Tuple<object, string> pxp = pxp_source_xpathf_dic[option_dic["key"]];

         if (typeof(bool).Equals(value.GetType()))
            XPathy.SetXPaths(pxp.Item1, pxp.Item2, ((value as Nullable<bool>) ?? false) ? "true" : "false");
         else
            XPathy.SetXPaths(pxp.Item1, pxp.Item2, value as string);
      }

      foreach (Type t in targetTypes)
      {
         object o = null;
         try { o = System.Convert.ChangeType(value, t); }
         catch { o = null; }
         finally { l.Add(o); }
      }

      return l.ToArray();
   }
}

ConvertBack() 函数的主要目的是将传入的值设置到缓存的源 XmlDataProvider 中的缓存的展开 XPath。 但是为了遵循协议,我们必须返回一个值数组。 通常,源绑定将是“OneWay”的,并且不会利用数组中的返回值。 但是,如果需要,此函数会半心半意地尝试满足此约定。

与 Convert() 函数一样,如果存在转换器选项,则会从 ConverterParameter 中提取它们。 XPath 部分的 ConverterParameter 被忽略,而使用缓存的展开形式。 “Key”选项用于查找先前缓存的源 XmlDataProvider 和 XPath 的展开形式。 如果满足先前的约束条件,则会在源 XML 中设置该值。

最后,为了在源绑定需要时返回值,会进行简单的尝试。

结论

我期望此类能够继续满足我在标准 WPF 数据绑定之外的参数化 XPath 需求,并希望它也能为您提供价值。 我已将该类、此处 提供的支持 SetXPath() 和 TapXPath() 函数打包到一个独立的 .cs 源文件中。 如果您有任何改进建议,请告诉我。

 

© . All rights reserved.