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

在 WPF 中使用 ValueConverter 和 MultiValueConverter

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.19/5 (7投票s)

2011年12月13日

CPOL

9分钟阅读

viewsIcon

64213

downloadIcon

719

WPF中用于输入基于单位值的示例值转换器和多值转换器。

引言

C#中的Windows Presentation Foundation (WPF) 包含了一些技术和接口,用于将控件和属性相互关联(这里似乎有个拼写错误,原文为"eahm",我将其视为“将”)。用户可以输入一个值,并(或其反馈)将其显示在GUI的某个位置。本文介绍了一种通过XAML中的ValueConverterMultiValueConverter来检查有效输入值的方法。

背景

本文的背景是一个使用了Ionic zip文件处理及其文件分割或文件跨距功能的应用程序。此功能允许用户创建一系列zip文件,每个文件的大小大致相同。例如,用户可以创建一系列5个文件,每个文件最大100MB。如果文件的总大小可能非常大,但对单个文件的大小有限制,这会很有用。

当有人想将zip文件存储在大小有限的媒体上(例如:U盘),或者压缩数据大小超过2GB(在Windows XP上)或4GB(在Vista/Windows 7上)时,就会出现上界文件跨距/文件大小问题。

本文展示了一种灵活解决此问题的方法。用户可以输入任意单位的内存大小(例如:xxx 字节、KB、MB、GB、TB 或 PB),软件将根据语法和实际值(在一定范围内)来验证输入数据是否有效。

正如在图1和图2中所见,在我提供的示例测试应用程序中,有效的内存范围被设置为100MB到4GB之间(包含这两个值)。

图1:包含的测试应用程序中的TestWindow的示例截图

图2:包含的测试应用程序中的TestDialog的示例截图

图1和图2中的输出显示了不同的开发阶段,其中输入的数值被转换并显示为不同的反馈。

图1中的TestWindow显示了第一个开发阶段,用户可以输入“100GB”或“1024 KB”之类的字符串,程序会将此字符串转换为字节值,并显示在主输入TextBox控件下方的TextBlock中。第二个TextBlock(在第一个TextBlock下方,带有合适的单位)显示了从第一个字节TextBlock转换器的输出。这两个输出的目的是在用户输入和修改值时可视化转换结果。

第一个输出是必需的,因为检查一个值是否在给定范围内需要将其转换为最小可能单位(在本例中为字节)的值。第二个标签很有用,因为用户不应该看到像“请选择一个小于10048 MB的值”这样的消息,当上限被超过时。因此,为了输出目的,需要进行适当的大小转换。

但是我们有些言过其实了。让我们从底到顶来研究一下这个项目并开发解决方案。

使用代码

StyleCop

我在项目中使用StyleCop以统一的方式使代码可读。因此,如果您在编译项目时遇到错误,可以下载并安装StyleCop,或者编辑/删除每个.csproj文件中的相应条目。

<Import Project="$(ProgramFiles)\MSBuild\StyleCop\v4.5\StyleCop.Targets" /> 

项目结构

图3中的图像显示了项目的粗略结构。有一个FileSize_Converter_Test项目和一个FSControls项目。前者是我们想在Visual Studio中设置为启动项目以测试FSControls功能所需项目。FSControls库包含本文档所述的解析器和转换器功能。这个DLL可以包含并在其他项目中消耗。

图3:提交的源代码的项目结构

FSControls项目的核心是FileSizeContextFileSizeExpressionFileSizeParserTemrinalFileSizeExpression类。

FileSizeExpression类定义了代表每个单元的类的基本原理。每个类

  • BytesFileSizeExpression
  • KbFileSizeExpression
  • MbFileSizeExpression
  • GbFileSizeExpression
  • TbFileSizeExpression

FileSizeContext类中的TerminalFileSizeExpression派生而来,代表内存大小的相应单位(字节、KB、MB、GB...)。

FileSizeContext类中的ConvertUnparsedSizeToBytesInt函数可以用来检查将包含“100MB”、“10 bytes”或“1024 GB”之类的字符串转换为int字节数是否能够成功。如果您想有一个简单的转换过程参考,请查看此函数。

public static bool ConvertUnparsedSizeToBytesInt(object value,
                                                     out ulong numberOfBytes,
                                                     out string[] sInputComponents)

TerminalFileSizeExpression类从FileSizeContext类派生,并定义了Interpret方法。Interpret方法检查当前类的ThisPattern(例如:“GB”)是否可以匹配到字符串的末尾,如果匹配成功,则将其值转换为该类的NextPattern(例如:“MB”)。

FileSizeParser类也从TerminalFileSizeExpression类派生,并重写了其自己的Interpret方法版本。在此方法中,我们遍历每个单位(从上到下“TB”到“bytes”),如果当前单位与存储在value对象中的输入字符串匹配,则将其“向下”转换为下一个单位。

public override void Interpret(FileSizeContext value, out string[] strComponents)
{
  strComponents = null;

  // Go through each defined expression and see if we can match it with a known unit
  // Compute next smaller unit (eg. convert from Gb to Mb) and keep going until we
  // hit bytes
  foreach (FileSizeExpression exp in this.expressionTree)
  {
    string[] strOutComponents = null;
      
    // Adjust value to next smaller unit (if any)
    exp.Interpret(value, out strOutComponents);

    // Found a match and converted OK -> Return to sender
    if (exp.InterpretOK == true)
    {
      // return recognized and cleaned string components from input
      if (this.InterpretOK == false)
        strComponents = strOutComponents;

      this.InterpretOK = true;
    }
  }

  // Attempt 'bytes conversion' as last resort
  if (this.InterpretOK == false)
    this.InterpretAsBytes(value);
}

调用InterpretAsBytes方法是必要的,因为用户可能输入“1000 bytes”,而我们希望处理该输入。在这种情况下,无法“向下”转换,但输入仍然有效,所以我们使用此方法来解析该输入。

转换器

通常,转换器位于Window(或UserControl)的资源中,并通过其x:key属性进行引用。

<Window.Resources>
  <local:NumberToBestSizeConverter2 x:Key="MyConverter" />
</Window.Resources>

但是,当您开始使用几个转换器时,这很快就会变得乏味。此实现基于一种替代方案,即转换器从MarkupExtension类派生。这种派生使我们能够直接在XAML中使用生成的转换器(而无需显式引用现有资源)。

IValueConverter

IValueConverter实现的示例可以在FSControls/FileSize/Converter包中找到。此包中的每个文件都包含两个类,一个IValueConverter和一个IMultiValueConverter(请参见下一节),它们具有相同的转换目的。

IValueConverter接口乍一看可能显得复杂且难以理解。但它的目的实际上只是接收一个输入值并将其转换为一个输出值。通过在Convert函数开头插入断点,并在下面讨论的XAML中使用它时进行单步调试,可以最好地验证这一点。

NumberToBestSizeConverter类中的IValueConverter例如将“1024 KB”之类的简单字符串转换为人类可读的输出字符串“1 MB”。NumberToBestSizeConverter2类执行相同的操作,但它还接受一个可选参数(类型为ulongarray[2]),以确定输入的数值是否在提供的数组的最小-最大范围内。

IMultiValueConverter

IMultiValueConverter实际上就是一个IValueConverter,它接受一个对象数组作为输入并输出一个转换后的对象。

NumberToBestSizeConverter3类所做的与NumberToBestSizeConverter2类类似,除了它仅在出现语法错误或建议的易读字符串实际上与输入字符串不同时才打印输出值。

这在TestDialog类(详见下文)中用于可视化反馈,仅在

  • 出现问题时,或者
  • 用户实际输入“100000 bytes”并希望软件告诉他们这相当于多少GB时。

NumberToBestSizeConverter3类从IMultiValueConverter派生,以接受对象数组作为输入。这是必需的,以支持通过组合框选择单位并通过文本框输入值。

TestWindow

查看TestWindow类中的XAML,以了解如何将文本框中的输入字符串转换为字节表示(NumberToSizeConverter),然后再转换为“BestSize”表示(NumberToBestSizeConverter)。请注意Window.Resources标签。

<Window.Resources>
  <x:Array Type="{x:Type sys:UInt64}" x:Key="MinMaxValues">
    <sys:UInt64>209715200</sys:UInt64><!-- Minimum is 200Mb-->
    <sys:UInt64>4294967296</sys:UInt64><!-- Maximum 4Gb -->
  </x:Array>
</Window.Resources>

UInt64对象数组用于通过相应的Converter绑定语句的ConverterParameter来确定可接受值的范围。例如,如下所示:

<TextBox x:Name="txtBytes"
          IsReadOnly="True" BorderThickness="0" Margin="3"
          Grid.Column="1" Grid.Row="1" HorizontalAlignment="Left" Width="350">
  <TextBox.Text>
    <Binding ElementName="txtInput" Path="Text" Converter="{local:NumberToSizeConverter}"
             ConverterParameter="{StaticResource MinMaxValues}" />
  </TextBox.Text>
</TextBox>

请注意附加到“Test Click”按钮IsEnabled属性的转换器。它确保按钮仅在输入的数值有效且在定义的范围内时才可点击。

这里真的没有太多要说的了。只需在任何转换器的相应转换函数中设置一个断点,如果您对绑定和其他所有复杂内容有疑问,请调试该应用程序。

TestDialog

TestDialog类是第二个方法,它包含一些更高级的项目,例如:

  • 一个ViewModel(ProgrammOptionsVM)用于绑定,
  • 一个组合框用于输入内存单位的大小,
  • 一个在出现输入错误时弹出的图像,
  • 一个仅在“BestSize”与输入不同或发生错误时才显示的标签,
  • 以及一个仅在输入的数值有效时才启用的“OK”按钮。

虽然所有这些东西一开始可能令人困惑,但它们主要基于不同的转换器,而转换器又依赖于TestDialog.xaml文件中Window.Resources标签中定义的那些对象。

下面是控制是否显示错误图像的示例XAML:

<Image Source="/FileSize_Converter_Test;component/icons/error.png"
           Width="32" Height="32" Grid.Column="0" Grid.Row="2"
           HorizontalAlignment="Right" VerticalAlignment="Center">
  <Image.Visibility>
    <MultiBinding Converter="{local:SizeNumberToErrorVisibilityConverter2}"
                  ConverterParameter="{StaticResource MockupMinMaxValues}"
                  Mode="OneWay">
      <Binding ElementName="txtInput" Path="Text"/>
      <Binding ElementName="cmdMemUnits" Path="SelectedItem.Value.Value" />
    </MultiBinding>
  </Image.Visibility>
</Image>

可见性属性绑定到一个MultiValue转换器SizeNumberToErrorVisibilityConverter2。转换器从MockupMinMaxValues UInt64值数组获取其参数。要转换的实际object[]值来自文本框(txtInput)和组合框(cmdMemUnits)。

请注意TestWindow.xaml.cs文件中的btnTestWindow1_Click方法。此方法显示了“Mockup”Windows资源如何在运行时被覆盖(用真实对象及其值)。

ProgramOptionsVM ViewModel没有模型。通常,我们会设计一个模型类,例如ProgramOptions,它只包含selectedMemUnitselectedSize的属性。这样,该模型就可以在不直接依赖视图的情况下进行持久化和处理。但为了简化,这里省略了这一点。相反,“模型”的追溯终止于ProgramOptionsVM类的字段。

private readonly Dictionary<MemUnit, UIList> mMemUnits;
private MemUnit selectedMemUnit;
private double selectedSize;

上述字段随后通过其相应的属性暴露并绑定到TestDialog.xaml控件。

关注点

这个项目让我对转换器和ViewModel之间的关系以及如何使用它们相互交互有了深刻的认识。我注意到每个转换器也可以通过ViewModel类中的转换函数和输出属性来实现。有时,这可能也是一种可行的方法,因为生成的代码可能更容易理解和维护。

我想展示一些比通常从别处复制的布尔值到可见性的东西更具新颖性的转换器。所以,就这样吧。请让我知道您的想法和评论。

参考文献

本文基于以下资源:

历史

  • 2011.12.12:初稿。
  • 2011.12.13:更新了文章,包含了一个关于实际MultiValueConverter绑定的示例,该绑定在源代码中使用。更新了源代码以修复内存大小输入值绑定中的一个问题。我希望支持浮点值(例如1.5 GB)。因此,我将SelectedMemSize属性更改为字符串,并通过ProgramOptionsVM类中的一个单独属性(SelectedMemSizeNumberOfBytes)计算实际字节大小。
© . All rights reserved.