在 WPF 中使用 ValueConverter 和 MultiValueConverter






4.19/5 (7投票s)
WPF中用于输入基于单位值的示例值转换器和多值转换器。
引言
C#中的Windows Presentation Foundation (WPF) 包含了一些技术和接口,用于将控件和属性相互关联(这里似乎有个拼写错误,原文为"eahm",我将其视为“将”)。用户可以输入一个值,并(或其反馈)将其显示在GUI的某个位置。本文介绍了一种通过XAML中的ValueConverter
和MultiValueConverter
来检查有效输入值的方法。
背景
本文的背景是一个使用了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和图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可以包含并在其他项目中消耗。
FSControls项目的核心是FileSizeContext
、FileSizeExpression
、FileSizeParser
和TemrinalFileSizeExpression
类。
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
类执行相同的操作,但它还接受一个可选参数(类型为ulong
的array[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
,它只包含selectedMemUnit
和selectedSize
的属性。这样,该模型就可以在不直接依赖视图的情况下进行持久化和处理。但为了简化,这里省略了这一点。相反,“模型”的追溯终止于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
)计算实际字节大小。