Archive

Archive for February, 2011

Data Binding and StringFormat in Silverlight

February 14, 2011 5 comments

Sometimes, when you are designing screens for Silverlight, it is nice to use the StringFormat feature to get the values to show up correctly. This paradigm only works when you are using strongly typed objects. Consider the following example of a strongly typed model:

public class TypedEntity : INotifyPropertyChanged
{
    #region Properties

    private string _employeeName;
    public string EmployeeName
    {
        get { return _employeeName; }
        set
        {
            _employeeName = value;
            OnPropertyChanged(() => EmployeeName);
        }
    }

    private DateTime _startDate;
    public DateTime StartDate
    {
        get { return _startDate; }
        set
        {
            _startDate = value;
            OnPropertyChanged(() => StartDate);
        }
    }

    private double _salary;
    public double Salary
    {
        get { return _salary; }
        set
        {
            _salary = value;
            OnPropertyChanged(() => Salary);
        }
    }

    private double _numberOfDependents;
    public double NumberOfDependents
    {
        get { return _numberOfDependents; }
        set
        {
            _numberOfDependents = value;
            OnPropertyChanged(() => NumberOfDependents);
        }
    }

    #endregion

    #region INotifyPropertyChanged Members

	// ...

    #endregion
};

As you can see, it uses the correct corresponding type so that StringFormat will as expected. Now let’s look at the corresponding Xaml:

        <TextBlock Grid.Row="0" Grid.Column="3" VerticalAlignment="Center" Text="Using StringFormat strongly typed" Grid.ColumnSpan="2" FontSize="16" />
        <TextBlock Grid.Row="1" Grid.Column="3" VerticalAlignment="Center" Text="Employee Name:" />
        <TextBlock Grid.Row="2" Grid.Column="3" VerticalAlignment="Center" Text="Start Date:" />
        <TextBlock Grid.Row="3" Grid.Column="3" VerticalAlignment="Center" Text="Salary:" />
        <TextBlock Grid.Row="4" Grid.Column="3" VerticalAlignment="Center" Text="Number of Dependents:" />
        <TextBox Grid.Row="1" Grid.Column="4" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding TypedEntity.EmployeeName, Mode=TwoWay}" />
        <TextBox Grid.Row="2" Grid.Column="4" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding TypedEntity.StartDate, Mode=TwoWay, StringFormat=d}" />
        <TextBox Grid.Row="3" Grid.Column="4" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding TypedEntity.Salary, Mode=TwoWay, StringFormat=c}" />
        <TextBox Grid.Row="4" Grid.Column="4" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding TypedEntity.NumberOfDependents, Mode=TwoWay, StringFormat=n0}" />

This will render the following output:

Now let’s see where this falls down when using a non-strongly typed object model. Before you start saying, why don’t you plan ahead and make sure that you are data binding to strongly typed objects, there are scenarios when we have an object or table that acts as a property bag for clients in our applications. We still want to provide them with formatting but it is not as intuitive as using what we currently have. Let’s first look at our mode:

public class Entity : INotifyPropertyChanged
{
    #region Properties

    private string _employeeName;
    public string EmployeeName
    {
        get { return _employeeName; }
        set
        {
            _employeeName = value;
            OnPropertyChanged(() => EmployeeName);
        }
    }

    private string _startDate;
    public string StartDate
    {
        get { return _startDate; }
        set
        {
            _startDate = value;
            OnPropertyChanged(() => StartDate);
        }
    }

    private string _salary;
    public string Salary
    {
        get { return _salary; }
        set
        {
            _salary = value;
            OnPropertyChanged(() => Salary);
        }
    }

    private string _numberOfDependents;
    public string NumberOfDependents
    {
        get { return _numberOfDependents; }
        set
        {
            _numberOfDependents = value;
            OnPropertyChanged(() => NumberOfDependents);
        }
    }

    #endregion

    #region INotifyPropertyChanged Members

	// ...

    #endregion
};

As you can see, the model is basically the same with one exception, all of the properties are of type string. Here is the Xaml using the StringFormat paradigm:

<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Text="Using StringFormat" Grid.ColumnSpan="2" FontSize="16" />
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Text="Employee Name:" />
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Text="Start Date:" />
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Text="Salary:" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Text="Number of Dependents:" />
<TextBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.EmployeeName, Mode=TwoWay}" />
<TextBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.StartDate, Mode=TwoWay, StringFormat=d}" />
<TextBox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.Salary, Mode=TwoWay, StringFormat=c}" />
<TextBox Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.NumberOfDependents, Mode=TwoWay, StringFormat=n0}" />

Here is what the output would be:

As you can, the data binding ignored the StringFormat. This is a problem. What if we fallback and use a ValueConverter? Let’s take a look at the code:

/// <summary>
/// Sample usage:
/// Text="{Binding ProjectBudget, Mode=TwoWay, Converter={StaticResource StringFormatConverter}, ConverterParameter='\{0:C2\}'}"
/// </summary>
public class StringFormatConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (parameter != null)
        {
            string formatString = parameter.ToString();
            if (!string.IsNullOrEmpty(formatString))
            {
                string strValue = value as string;
                if (strValue != null && !string.IsNullOrWhiteSpace(strValue))
                {
                    return String.Format(formatString, strValue);
                }
            }
        }

        return value.ToString();
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value;
    }

    #endregion
};

So basically, this is trying to do what we get in our Xaml natively but now using the String.Format method. Let’s look at our modified Xaml to see how this looks:

<TextBlock Grid.Row="5" Grid.Column="0" VerticalAlignment="Center" Text="Using a ValueConverter" Grid.ColumnSpan="2" FontSize="16" Margin="0,10,0,0" />
<TextBlock Grid.Row="6" Grid.Column="0" VerticalAlignment="Center" Text="Employee Name:" />
<TextBlock Grid.Row="7" Grid.Column="0" VerticalAlignment="Center" Text="Start Date:" />
<TextBlock Grid.Row="8" Grid.Column="0" VerticalAlignment="Center" Text="Salary:" />
<TextBlock Grid.Row="9" Grid.Column="0" VerticalAlignment="Center" Text="Number of Dependents:" />
<TextBox Grid.Row="6" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.EmployeeName, Mode=TwoWay}" />
<TextBox Grid.Row="7" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.StartDate, Mode=TwoWay, Converter={StaticResource StringFormatConverter}, ConverterParameter='\{0:d\}'}" />
<TextBox Grid.Row="8" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.Salary, Mode=TwoWay, Converter={StaticResource StringFormatConverter}, ConverterParameter='\{0:c\}'}" />
<TextBox Grid.Row="9" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.NumberOfDependents, Mode=TwoWay, Converter={StaticResource StringFormatConverter}, ConverterParameter='\{0:n0\}'}" />

Now that wasn’t too bad, let’s look and see what the output gives us:

As you can see, this doesn’t work either. It turns out that String.Format relies on the underlying type in order for it to work correctly.

Well, thankfully, .NET comes to the rescue and we get the following converter:

/// <summary>
/// Sample usage:
/// Text="{Binding Budget, Mode=TwoWay, Converter={StaticResource StringFormatConverterWithChangeType}, ConverterParameter='\{0:C2\}'}"
/// </summary>
public class StringFormatConverterWithChangeType : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value != null)
        {
            // Arbitrarily remove dollar sign.
            value = value.ToString().Replace("$", "");
        }

        if (parameter != null)
        {
            string formatString = parameter.ToString();
            if (!string.IsNullOrEmpty(formatString))
            {
                string strValue = value as string;
                if (strValue != null && !string.IsNullOrWhiteSpace(strValue))
                {
                    switch (formatString)
                    {
                        case "{0:n}":   // Number.
                        case "{0:n0}":  // Whole Number.
                        case "{0:c}":   // Currency.
                        case "{0:c0}":  // Whole currency.
                        case "{0:C2}":  // Whole currency.
                            return String.Format(culture, formatString, System.Convert.ChangeType(value, typeof(double), null));
                        case "{0:d}":  // Short date.
                        case "{0:D}":   // Long date.
                            return String.Format(culture, formatString, System.Convert.ChangeType(value, typeof(DateTime), null));
                    }
                }
                else
                {
                    return String.Format(culture, formatString, value);
                }
            }
        }

        return value.ToString();
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value;
    }

    #endregion
};

Perhaps on first glance, you don’t see the difference between the two converters but then as you look more carefully, you will see that we are using the System.Convert.ChangeType method to coerce our underlying property to be of a certain type. I will warn you that this solution will not work for all scenarios and you should read up here to get more information.

Here is the Xaml now using the new converter:

<TextBlock Grid.Row="10" Grid.Column="0" VerticalAlignment="Center" Text="Using a ValueConverter with Convert class" Grid.ColumnSpan="2" FontSize="16" Margin="0,10,0,0" />
<TextBlock Grid.Row="11" Grid.Column="0" VerticalAlignment="Center" Text="Employee Name:" />
<TextBlock Grid.Row="12" Grid.Column="0" VerticalAlignment="Center" Text="Start Date:" />
<TextBlock Grid.Row="13" Grid.Column="0" VerticalAlignment="Center" Text="Salary:" />
<TextBlock Grid.Row="14" Grid.Column="0" VerticalAlignment="Center" Text="Number of Dependents:" />
<TextBox Grid.Row="11" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.EmployeeName, Mode=TwoWay}" />
<TextBox Grid.Row="12" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.StartDate, Mode=TwoWay, Converter={StaticResource StringFormatConverterWithChangeType}, ConverterParameter='\{0:d\}'}" />
<TextBox Grid.Row="13" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.Salary, Mode=TwoWay, Converter={StaticResource StringFormatConverterWithChangeType}, ConverterParameter='\{0:c\}'}" />
<TextBox Grid.Row="14" Grid.Column="1" HorizontalAlignment="Left" Width="125" Margin="5,5,0,0" Text="{Binding Entity.NumberOfDependents, Mode=TwoWay, Converter={StaticResource StringFormatConverterWithChangeType}, ConverterParameter='\{0:n0\}'}" />

And finally, here is the output we want:

So the moral of the story is that if you have some screen that needs to be dynamic and you don’t know ahead of time what user is going to use the property for, you may want to go this route and let your value converter do the parsing. I have a designer screen that allow users to bind to a property bag of strings but they can define what “type” they want to use the property for. By doing this, I can generate the correct Xaml on the fly and render it and bind to it as if it were strongly typed.

You can get download a sample project from here.

Also, you can read more on the syntax for formatting here and here.

Hope this helps….