wpf 在绑定中绑定XPath

mwg9r5ms  于 2023-06-07  发布在  其他
关注(0)|答案(2)|浏览(291)

我一直在寻找,寻找和尝试多种东西,只是没有能够弄清楚。这似乎应该是直截了当的,但我不知所措。我正试图从另一个文本框中提取基于值的数据。
它提取的基本XML:

<?xml version="1.0" encoding="UTF-8"?>
<Translations>
  <Translation Id="Greeting">
    <English>Hello</English>
    <French>Bonjour</French>
</Translation>

XAML:

<Window.Resources>
  <XmlDataProvider x:Key="TranslationData" Source="F:\Translations.xml" XPath="Translations"/>
</Window.Resources>

<TextBox Name="LanguageTextBox" Text="English" Width="200" />
<TextBox Name="Results" Text="{Binding Source={StaticResource TranslationData}, XPath=*[@Id\=\'Greeting\']/English}" Width="200" />

我尝试在XPath语句中替换单词'英语',以便它使用LanguageTextBox的值,因此,如果它是'English',它将保持不变,但当更改为'French'时,XPath将如下所示:

{Binding Source={StaticResource TranslationData}, XPath=*[@Id\=\'Greeting\']/French}
kxkpmulp

kxkpmulp1#

底层问题

在您的示例中,您得到了一个根据所选语言而变化的 XPath 查询。这也需要在XPath属性上进行绑定,以便对语言选择TextBox中的更改值做出React,从而创建新的 XPath。但是,这是不可能的,因为Binding类型中的XPath属性不是dependency property,而是普通CLR属性,因此它不支持数据绑定。

依赖属性可以通过数据绑定引用值。数据绑定通过XAML中的特定标记扩展语法或代码中的Binding对象工作。使用数据绑定时,最终属性值的确定将推迟到运行时,此时将从数据源获取值。

一种数值转换器解决方案

您可以使用value converter,而不是在绑定中使用 XPath,它使用您的XmlDataProvider来检索 languagekey 的文本。您也可以直接加载XmlDocument,而不需要数据提供程序。两者都支持通过 XPath 进行查询。
您可以编写常规的IValueConverterIMultiValueConverter(支持绑定多个值)。由于您的密钥似乎是硬编码的,我将提供更简单的常规变体,其中密钥可以作为不可绑定的转换器参数提供。

public class TranslateConverter : IValueConverter
{
   private XmlDataProvider _dataProvider;

   public XmlDataProvider DataProvider
   {
      get => _dataProvider;
      set
      {
         _dataProvider = value;
         ForceLoadXmlDocument();
      }
   }

   public string FallbackText { get; set; }

   public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
   {
      if (DataProvider is null)
         throw new InvalidOperationException("No XML data provider set.");

      if (DataProvider.Document is null)
         throw new InvalidOperationException("No XML document loaded.");

      if (!(value is string language) || !(parameter is string key))
         return Binding.DoNothing;

      if (string.IsNullOrWhiteSpace(language) || string.IsNullOrWhiteSpace(key))
         return FallbackText;

      return SelectLanguageText(language, key) ?? FallbackText;
   }

   public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
   {
      throw new InvalidOperationException();
   }

   private void ForceLoadXmlDocument()
   {
      if (_dataProvider == null || _dataProvider.Document != null)
         return;

      _dataProvider.IsInitialLoadEnabled = true;
      _dataProvider.IsAsynchronous = false;
      _dataProvider.Refresh();
   }

   private string SelectLanguageText(string language, string key)
   {
      var xpath = $"//*[@Id='{key}']/{language}";
      return DataProvider.Document.SelectSingleNode(xpath)?.InnerText;
   }
}

大多数代码都包含参数有效性的检查。在核心SelectLanguageText方法中,使用 XPath 查询来自数据提供程序的文本,就像在XAML中一样。如果没有翻译,则返回回退文本。ForceLoadXmlDocument方法用作workaround,因为数据提供程序可能不会立即加载XML文档。
在XAML中,您将定义转换器的示例并在绑定中使用它。

<Window.Resources>
   <XmlDataProvider x:Key="TranslationData" Source="/Translations.xml" XPath="Translations"/>
   <local:TranslateConverter x:Key="TranslateConverter" DataProvider="{StaticResource TranslationData}" FallbackText="# No translation found! #"/>
</Window.Resources>
<TextBox Name="LanguageTextBox" Text="English" Width="200" />
<TextBox Name="Results" Text="{Binding Text, ElementName=LanguageTextBox, Converter={StaticResource TranslateConverter}, ConverterParameter=Greeting}" Width="200"/>

替代方案

您可以创建一个专用的翻译提供程序类型,它公开一个通过绑定设置的属性Language,就像您的示例中的TextBox一样。

<TextBox Name="LanguageTextBox" Text="{Binding Language, Source={StaticResource TranslationProvider}}" />

然后你可以暴露一个indexer。可以与用于翻译的密钥绑定。

<TextBox Name="LanguageTextBox" Text="{Binding [Greeting], Source={StaticResource TranslationProvider}}" />

其逻辑与直接使用数据提供程序或XML文档的转换器非常相似。

备注

  • 不建议使用TextBox输入语言。它允许任何值,并需要额外的验证(请参阅转换器中的null或空格检查)。您应该使用ComboBox或任何其他仅提供可供选择的不同有效值的控件。
  • 我觉得你的翻译文件结构很奇怪。您为一个键提供了一个翻译元素,该元素包含每种语言的值。您更希望为每个键都有一个翻译的语言元素。这使您能够独立地发送语言文件。
  • 如果语言文件不是本地的,就把它放到应用程序资源中,否则会在每个窗口中得到相同字典的冗余示例。
d6kp6zgx

d6kp6zgx2#

如果你想直接绑定到一个XML文档,你必须使用一个转换器。或者将XML文档反序列化为类。
因为绑定结果基于两个输入(XML文档和用户输入),所以必须使用MultiBinding

<Window.Resources>
  <XmlDataProvider x:Key="TranslationData" 
                   Source="F:\Translations.xml" 
                   XPath="Translations" />
</Window.Resources>

<TextBox x:Name="LanguageTextBox" 
         Text="English" />
<TextBox>
  <TextBox.Text>
    <MultiBinding Mode="OneWay">
      <MultiBinding.Converter>
        <local:XmlDocumentToElementInnerTextConverter />
      </MultiBinding.Converter>

      <Binding ElementName="LanguageTextBox"
               Path="Text" />
      <Binding Source="{StaticResource TranslationData}" />
    </MultiBinding>
  </TextBox.Text>
</TextBox>
public class XmlDocumentToElementInnerTextConverter : IMultiValueConverter
{
  public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  {
    if (values.OfType<IList>().FirstOrDefault() is IList nodeList)
    {
      XmlElement? xmlRootElement = nodeList
        .OfType<XmlElement>()
        .FirstOrDefault();
      if (xmlRootElement is not null
        && values.OfType<string>().FirstOrDefault() is string languageInput)
      {
        // Enforce capital first char to match XML tag convention
        string normalizedLanguageInput = $"{languageInput[0].ToString().ToUpper()}{languageInput[1..]}";

        XmlNodeList matchingNodes = xmlRootElement.GetElementsByTagName(normalizedLanguageInput);
        return matchingNodes?.OfType<XmlElement>().FirstOrDefault()?.InnerText
          ?? string.Empty;
      }
    }

    return string.Empty;
  }

  public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 
    => throw new NotSupportedException();
}

以下示例显示如何基于用户选择的属性值(Translation.Id)提取XML对象。
Translation XML对象的IdMap到翻译类别(实际属性值),该类别使用ComboBox呈现给用户。该语言还使用ComboBox呈现给用户。
基于所选择的值,我们提取所选择的类别的适当翻译(例如Greeting)和语言(例如English)。
这个例子使用LINQ to XML而不是XmlDataProvider来更方便地遍历XML文档树:

Translations.xml

<?xml version="1.0" encoding="UTF-8"?>
<Translations>
  <Translation Id="Greeting">
    <English>Hello</English>
    <French>Bonjour</French>
  </Translation>
  <Translation Id="Goodbye">
    <English>Goodbye</English>
    <French>Au revoir</French>
  </Translation>
</Translations>

MainWindow.xaml

<Window>
  <StackPanel>
    <ComboBox x:Name="CategorySelector"
              SelectionChanged="OnSelectionChanged" />
    <ComboBox x:Name="LanguageSelector"
              SelectionChanged="OnSelectionChanged" />
    <TextBlock Name="TranslatedValue" />
  </StackPanel>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  private XElement XmlDocument { get; }

  public MainWindow()
  {
    InitializeComponent();

    // Load the XML document into memory
    this.XmlDocument = XElement.Load("f:/translations.xml");

    // Populate the category picker ComboBox 
    // with all categories found in the XML (Id values)
    this.CategorySelector.ItemsSource = this.XmlDocument
      .Descendants("Translation")
      .Select(translationNode => translationNode.Attribute("Id"))
      .Select(translationNodeAttribute => translationNodeAttribute.Value);

    // Populate the language picker ComboBox 
    // with all languages found in the XML
    this.LanguageSelector.ItemsSource = this.XmlDocument
      .Descendants("Translation")
      .SelectMany(translationNode => translationNode.Elements())
      .Select(languageNodes => languageNodes.Name.ToString())
      .Distinct();

    // Preselect a default category value 
    this.CategorySelector.SelectedIndex = 0;

    // Preselect a language value
    this.LanguageSelector.SelectedIndex = 0;
  }

  // Extract the new translation value from the XML document
  // when the category or language ComboBox has a new selection
  private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  {
    SelectCurrentTranslation(this.CategorySelector.SelectedItem as string, this.LanguageSelector.SelectedItem as string);
  }

  // Extract the translated value based on category and language
  private void SelectCurrentTranslation(string? category, string? language)
  {
    if (category is null || language is null)
    {
      return;
    }

    var translatedValue = this.XmlDocument
      .Descendants("Translation")
      .First(translationNode => translationNode.Attribute("Id").Value.Equals(category, StringComparison.InvariantCultureIgnoreCase))
      .Elements()
      .First(languageNode => languageNode.Name.ToString().Equals(language, StringComparison.InvariantCultureIgnoreCase))
      .Value;

    // Display the translation in teh TextBlock
    this.TranslatedValue.Text = translatedValue;
  }
}

相关问题