Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using ValueConverter and MultiValueConverter in WPF

0.00/5 (No votes)
11 Jun 2012 1  
Sample WPF value and multivalue converters for entering unit based values.

Introduction

The Windows Presentation Foundation (WPF) in C# contains a few technologies and interfaces to link eahm bind controls and properties to each other. Users can enter a value and make it (or some feedback of it) appear somewhere in the GUI. This article presents a way of checking for valid input values via ValueConverter and MultiValueConverter in XAML.

Background

The background of this article is an application that uses Ionic zip file handling with the file splitting or file spanning feature. This feature lets the user create a series of zip files where each file has roughly the same size. A user can, for example, create a series of 5 files, each having a maximum size of 100MB. This can be useful if the total size of files can be very large but if there is an upper bound for the size of individual files.

The upper bound-file spanning/file size problem arises when someone wants to store the zipped file on a media with limited size (e.g.: USB stick), or if the compressed data size exceeds 2GB (on Windows XP) or 4GB (on Vista/Windows 7).

This article showcases one way to solve this problem in a flexible manner. The user can enter memory sizes in any unit he wants (e.g.: xxx bytes, KB, MB, GB, TB, or PB) and the software verifies whether the entered data is valid in terms of the syntax and the actual value (in between a range).

The valid memory range in my sample test application is set to anything between 100MB and 4GB (including these values) as can be seen in Figures 1 and 2.

Figure 1: A sample screenshot of the TestWindow in the included test application

Figure 2: A sample screenshot of the TestDialog in the included test application

The outputs in Figures 1 and 2 show different development stages in which entered values are converted and displayed with different feedbacks.

The TestWindow in Figure 1 shows the first development step where a user can enter a string like "100GB" or "1024 KB" and the program converts this string into a bytes value in the TextBlock just below the main input TextBox control. The second TextBlock with the appropriate unit (below the first TextBlock) shows a converter output from the first bytes TextBlock. The aim of these two outputs is to visualize the conversion results as a user types and re-types values.

The first output was needed, because checking whether a value is inside a given range requires conversion to a value of the smallest possible unit (that is bytes in this case). And the second label was useful because a user should not have to read a message like: "Please select a value smaller than 10048 MB" when the upper bound was exceeded. Therefore, an appropriate size converison is required for output purposes.

But we are getting ahead of ourselves. Let us look at the project and develop the solution from bottom to top.

Using the code

StyleCop

I use StyleCop in my projects to make the code readable in a uniform way. So, you can either download and install StyleCop, or edit/remove the corresponding entries in each .csproj file, should you get an error when compiling the project:

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

Project structure

The image in Figure 3 shows the rough structure of the project. There is a FileSize_Converter_Test project and a FSControls project. The former project is what we need to Set as Startup Project in Visual Studio if we want to test the FSControls functionality. The FSControls library contains the parser and converter functions documented in this article. This DLL can be included and consumed in other projects.

Figure 3: The project structure of the submitted source code

The core of the FSControls project are the FileSizeContext, FileSizeExpression, FileSizeParser, and TemrinalFileSizeExpression classes.

The FileSizeExpression class defines the fundamentals of each class that represents a unit. Each of the classes:

  • BytesFileSizeExpression
  • KbFileSizeExpression
  • MbFileSizeExpression
  • GbFileSizeExpression
  • TbFileSizeExpression

derived from TerminalFileSizeExpression in the FileSizeContext class, represents a corresponding unit (bytes, KB, MB, GB...) of memory sizes.

The ConvertUnparsedSizeToBytesInt function in the FileSizeContext class can be used to check whether conversion of a string containing something like "100MB", "10 bytes", or "1024 GB" into an int number of bytes can be successful or not. Review this function if you want to have a simple reference for the required conversion process.

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

The TerminalFileSizeExpression class derives from the FileSizeContext class and defines the Interpret method. The Interpret method checks if ThisPattern of the current class (e.g.: "GB") can be matched to the end of the string, and converts the value into the NextPattern of that class (e.g.: "MB"), if that was the case.

The FileSizeParser class is also derived from the TerminalFileSizeExpression class and overrides its own version of the Interpret method. In this method, we cycle through each unit (form top "TB" to bottom "bytes") and convert "down to next unit" if the current unit was matched with the input string stored in the value object:

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);
}

The call to the InterpretAsBytes method is needed because a user might enter "1000 bytes" and we would like to work with that input. In this case, a "down" conversion is not possible but the input is still valid so we use this method to resolve that input.

Converter

Normally, converters live in a resource in a Window (or UserControl) and are referenced via their x:key property:

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

But this gets tedious pretty soon as one starts using a few converters. This implementation is based on an alternative scenario where converters are derived from the MarkupExtension class. This derivation allows us to use the resulting converters directly in the XAML (without explicit reference to an existing resource).

IValueConverter

Samples of IValueConverter implementations can be found in the FSControls/FileSize/Converter package. Each file in this package contains two classes, an IValueConverter and an IMultiValueConverter (see next section) implementation for the same purpose of conversion.

The IValueConverter interface appears complicated and difficult to understand at first sight. But its purpose is really just to take an input value and convert it into an output value. This is probably best verified if you insert a breakpoint at the beginning of the Convert function and step through it when it is used from within the XAML discussed below.

The IValueConverter in the NumberToBestSizeConverter class converts, for instance, a simple string like "1024 KB" into a human readable output string "1 MB". The NumberToBestSizeConverter2 class does the same but it also accepts an optional parameter (an array[2] of type ulong) to determine whether the entered value is within the Min-Max range of the supplied array.

IMultiValueConverter

An IMultiValueConverter is really just an IValueConverter that accepts an array of objects as input and outputs a converted object.

The NumberToBestSizeConverter3 class does a similar job as the NumberToBestSizeConverter2 class, except it prints an output value only, if there is a syntax error, or if the suggested human read-able string does in fact differ from the input string.

This is used in the TestDialog class (see further below) to visualize a feedback only:

  • if something's wrong, or
  • if the user actually entered "100000 bytes" hoping that the software would tell him how many GBs this might be.

The NumberToBestSizeConverter3 class is derived from IMultiValueConverter to accept an array of input objects. This was required to support the selection of a unit via combobox and the input of the value via textbox.

TestWindow

Review the XAML in the TestWindow class to understand how the input string from the textbox is converted into a bytes representation (NumberToSizeConverter) and from there into a "BestSize" representation (NumberToBestSizeConverter). Take a note of the Window.Resources tag:

<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>

This array of UInt64 objects is used to determine the range of acceptable values via ConverterParameter of the corresponding Converter binding statements. For example, like so:

<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>

Notice the converter attached to the IsEnabled property of the Test Click button. It makes sure the button is clickable only if the entered value is valid and within the defined range.

There is really not much more to say here. Just set a breakpoint in the corresponding convert function of any converter and debug the application, if you are in doubt of the bindings and all the other mumbo-jumbo here.

TestDialog

The TestDialog class is the second approach on the subject and it features some more advanced items, such as:

  • a ViewModel (ProgrammOptionsVM) to bind to,
  • a combobox to enter the size of memory unit with,
  • an image that pops-up whenever there is an input error,
  • a label that shows only if the "BestSize" differs from input or if there is an error,
  • and an OK button that enables only if the entered value is OK.

While all these things might be confusing at first, they are mostly based on different converters, which in turn rely on the objects defined in the Window.Resources tag in the TestDialog.xaml file.

Here is the sample XAML that controls whether an error image is shown or not:

<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>

The visibility property is bound to a MultiValue converter SizeNumberToErrorVisibilityConverter2. The converter gets its parameters from the MockupMinMaxValues array of UInt64 values. The actual object[] values to be converted are supplied from the textbox (txtInput) and the combobox (cmdMemUnits).

Take a note of the btnTestWindow1_Click method in the TestWindow.xaml.cs file. This method shows how the 'Mockup' Windows resources are overwritten (with real objects and their values) at runtime.

The ProgramOptionsVM ViewModel does not have a model. Normally, one would design a model class, for example, ProgramOptions, that would contain only the properties for selectedMemUnit and selectedSize. This way, that model can be persisted and processed without direct dependency on the view. But this is omitted in favour of simplification. Instead the trace to the "model" ends in the fields of the ProgramOptionsVM class:

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

The above fields are then exposed and bound to the TestDialog.xaml controls via their corresponding properties.

Points of interest

This project gave me a huge inside on how converters and ViewModels are related to each other and how we can use both of them to interact with each other. I noticed that each converter can also be realized via a conversion function and output property in the ViewModel class. And sometimes, this may be the way to go, as well, because the resulting code may be easier to understand and maintain.

I wanted to show off some more novel Converters than the usual bool to Visibility stuff copied from somewhere else. So, there you go. Please let me know about your thoughts and comments on this.

References

The article is based on the following resources:

History

  • 12.12.2011: Initial post.
  • 13.12.2011: Updated article to contain a sample on the actual MultiValueConverter binding used in the source code. Updated source code to bug fix a problem in the binding of the memory size input value. I wanted to support a floating point value (such as 1.5 GB). So I changed the SelectedMemSize property to string and compute the actual size of bytes via a separate property (SelectedMemSizeNumberOfBytes) in the ProgramOptionsVM class.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here