1

Assuming we have a viewmodel property MyMoney. How can I format it in its view xaml as a currency with no trailing zeroes?

For example:

MyMoney = 1; //$1
MyMoney = 1.2 //$1.2

I've tried the following in xaml (e.g. <TextBox Text="{Binding MyMoney, StringFormat=..."/>) but it doesn't satisfy all the conditions:

StringFormat=C shows currency but also trailing zeroes.
StringFormat=C0 shows currency but shows only the whole number.
StringFormat={}{0:0.##} does not show trailing zeroes but not as currency.
StringFormat={}{0:$0.##} does not show trailing zeroes but hard-coded $. We should be able to cater current locale/culture's currency.

Jan Paolo Go
  • 5,842
  • 4
  • 22
  • 50
  • 1
    Side note: make sure you understand that "C" formats *don't know* what currency your values are and use currency symbol for current locale... While often those two values match it is not necessary the case - i.e. if your transactions are in USD and you show page in JA-JP you'll get surprising "prices" :) You may need to do custom formatting anyway (manually picking currency symbol and combining with "g") – Alexei Levenkov Dec 14 '18 at 17:05
  • @AlexeiLevenkov Ah yes. let me update my question. Thanks for pointing that out :) – Jan Paolo Go Dec 14 '18 at 17:06
  • What about using converter? – Kaspar Dec 14 '18 at 17:42

3 Answers3

2

I think you pretty much need to use a converter here because of the requirement to remove trailing zeroes. This probably needs a bit more work but:

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;

namespace wpf_99
{
public class CurrencyFormatConverter : MarkupExtension, IValueConverter
{
// public double Multiplier { get; set; } You could pass parameters to properties.

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol + System.Convert.ToDecimal(value).ToString("0.##");
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string input = value.ToString();
        if(!char.IsDigit(input[0]))
        {
            input= input.Substring(1);
        }
        if(input.Length == 0)
        {
            return 0;
        }
        return Decimal.Parse(input);
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}
}

Usage

<TextBox Text="{Binding Money,UpdateSourceTrigger=PropertyChanged, Converter={local:CurrencyFormatConverter}}"/>
Andy
  • 11,864
  • 2
  • 17
  • 20
  • Thank you! I like this approach. What is the Multiplier for? – Jan Paolo Go Dec 15 '18 at 16:09
  • I copied the code from a multiplier converter to save time on the interfaces. :^) You could, however, pass the currency symbol you want as a parameter to such a property. Maybe as an optional parameter. – Andy Dec 15 '18 at 16:21
  • I see. Another question, why use `CultureInfo.CurrentCulture` instead of the `culture` parameter? – Jan Paolo Go Dec 15 '18 at 23:59
  • Thanks for this approach and I've accepted it as the answer as using `IValueConverter` seems to be a good/clean way to achieve custom formatting on wpf. – Jan Paolo Go Dec 16 '18 at 01:47
  • I've posted a new question to focus on other test cases for currency formatting such as negative numbers, different culture, different number separators, etc. https://stackoverflow.com/questions/53798761/c-sharp-format-as-currency-with-no-trailing-zero-decimal-numbers-consider-negat – Jan Paolo Go Dec 16 '18 at 01:49
0

Probably you would be knowing that the "C" in string format works according to the locale/culture set in your local machine.However to answer your question,I would suggest to put the trailing zeroes removal code at property level and keep the string formatting at xaml level as simple as possible.for e.g

At xaml level :

  <TextBox x:Name="TextBox_Curr" Height="50" Text="{Binding Money,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay,StringFormat={}{0:C}}" Margin="120,134,156,135"/>

At Model level (just a raw code):

  private decimal _money;
        public decimal Money
        {
            get { return _money; }
            set {

                 _money = value;
                _money.ToString("0.##");

                 NotifyPropertyChanged("Money"); }
        }

It worked for me while running the sample code.

Also if you do not wish to change the system settings you could probably go for a forced culture setting.

public static class Cultures
{
    public static readonly CultureInfo UnitedKingdom = 
        CultureInfo.GetCultureInfo("en-GB");
}
Then:

Money.ToString("C", Cultures.UnitedKingdom)
AlphaWarrior
  • 57
  • 1
  • 9
0

First, I would like to give credit to @Andy for his answer which led me to use IValueConverter.

I'm posting my solution which offers the following benefits:

  1. Leverage C#'s "C" format specifier

    a. Consider negative values (e.g. -1 --> ($1))

    b. Cater varying current locale/culture

  2. Bind to multiple data types (decimal, double, int, etc).

  3. Return DependencyProperty.UnsetValue when ConvertBack is unable to produce a value.

All of the above follow how StringFormat=c will behave in wpf (e.g. in a TextBox) except the converter removes trailing zeroes as desired.


public class CurrencyFormatConverter : MarkupExtension, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
        System.Convert.ToDecimal(value).ToCurrency(culture);

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        switch (Type.GetTypeCode(targetType))
        {
            case TypeCode.Decimal:
                return Decimal.TryParse(value.ToString(), NumberStyles.Currency, culture, out var @decimal)
                    ? @decimal
                    : DependencyProperty.UnsetValue;

            case TypeCode.Double:
                return Double.TryParse(value.ToString(), NumberStyles.Currency, culture, out var @double)
                    ? @double
                    : DependencyProperty.UnsetValue;

            case TypeCode.Int16:
                return Int16.TryParse(value.ToString(), NumberStyles.Currency, culture, out var int16)
                    ? int16
                    : DependencyProperty.UnsetValue;

            case TypeCode.Int32:
                return Int32.TryParse(value.ToString(), NumberStyles.Currency, culture, out var int32)
                    ? int32
                    : DependencyProperty.UnsetValue;

            case TypeCode.Int64:
                return Int64.TryParse(value.ToString(), NumberStyles.Currency, culture, out var int64)
                    ? int64
                    : DependencyProperty.UnsetValue;

            case TypeCode.Single:
                return Single.TryParse(value.ToString(), NumberStyles.Currency, culture, out var single)
                    ? single
                    : DependencyProperty.UnsetValue;

            case TypeCode.UInt16:
                return UInt16.TryParse(value.ToString(), NumberStyles.Currency, culture, out var uint16)
                    ? uint16
                    : DependencyProperty.UnsetValue;

            case TypeCode.UInt32:
                return UInt32.TryParse(value.ToString(), NumberStyles.Currency, culture, out var uint32)
                    ? uint32
                    : DependencyProperty.UnsetValue;

            case TypeCode.UInt64:
                return UInt64.TryParse(value.ToString(), NumberStyles.Currency, culture, out var uint64)
                    ? uint64
                    : DependencyProperty.UnsetValue;

            default:
                throw new NotSupportedException($"Converting currency string to target type {targetType} is not supported.");
        }
    }

    public override object ProvideValue(IServiceProvider serviceProvider) => this;
}

More info on ToCurrency here

public static class DecimalExtensions
{
    /// <summary>
    ///     Converts a numeric value to its equivalent currency string representation using the specified culture-specific format information.
    /// </summary>
    /// <param name="value">The value to be converted.</param>
    /// <param name="provider">An object that supplies culture-specific formatting information.</param>
    /// <returns>The currency string representation of the value as specified by <paramref name="provider" />.</returns>
    public static string ToCurrency(this decimal value, IFormatProvider provider) =>
        /// Use "1" (or "-1" if value is negative)
        /// as a placeholder for the actual value.
        (value < 0 ? -1 : 1)

        /// Format as a currency using the "C" format specifier.
        .ToString("C0", provider)

        /// Convert the absolute value to its string representation
        /// then replace the placeholder "1".
        /// We used absolute value since the negative sign
        /// is already converted to its string representation
        /// using the "C" format specifier.
        .Replace("1", Math.Abs(value).ToString("#,0.############################", provider));
}
Jan Paolo Go
  • 5,842
  • 4
  • 22
  • 50