Search code examples
c#wpfdecimalstring-formattingtruncate

WPF Decimal Custom Format without Rounding


MS documentation says it this is not possible... Hoping I missed something in the docs.

want to format decimal number as: -9,999.000 using StringFormat={}{0:#,0.000} works, but this "rounds" the input where as I want to "truncate" the input.

To test, in WPF form applied this string format to a TextBox bound to a decimal property.

When I enter test values, the actual formatted value doesnt match the expected formatted value as the format string automatically "rounds" the decimal values.

Enter     Expected     Actual
1111.999  1,111.999    1,111.999
1111.9999 1,111.999    1,112,000

MS documentation clearly states that "0" placeholders will always round. great, but I dont want that behavior.

Question: is there a format string to show 3 decimal places (with trailing zeros) without using the "0"? or is there a format string that truncates at 3 decimal places instead of rounding at 3 decimal places?

I did try StringFormat={}{0:#,0.###} but that removes trailing zeros. (and also StringFormat={}{F3}).

Enter     Expected     Actual
1111.100  1,111.100    1,111.1

closest solution I could find has been [https://stackoverflow.com/questions/16914224/wpf-textbox-to-enter-decimal-values] but none of the answers really deal with the rounding versus truncate.

UPDATE

xaml code where StringFormat is used.

 <TextBox Text="{Binding 
     TemplateNumber,
     StringFormat={}{0:F3},
     ValidatesOnDataErrors=True, 
     NotifyOnValidationError=True, 
     UpdateSourceTrigger=PropertyChanged, 
     Delay=500}"
     />

UPDATE 2

the IValueConverter Convert function to truncate instead of round...

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
   if (value == null)
      return string.Empty;

   int decimalPlaces = 3;  // HACK: hardcoded, needs to be parameter
//   decimal decimalShift = (decimal)Math.Pow(10, decimalPlaces);
   decimal decimalValue = (decimal)value;
//   decimal decimalWhole = Math.Truncate(decimalValue);
 //  decimal decimalFraction = Math.Truncate((decimalValue - decimalWhole) * decimalShift);

   string format;
   string result;

// simpler truncation approach
   decimal d = Math.Round(decimalValue, decimalPlaces, MidpointRounding.ToZero);
   format = "{0:N" + decimalPlaces.ToString() + "}";
   result = string.Format(format, d);

/*
   if (decimalPlaces > 0)
   {
      format = "{0:N0}.{1:" + new string('0', decimalPlaces) + "}";
      result = string.Format(format, decimalWhole, decimalFraction);
   }
   else // whole number only
    {
       format = "{0:N0}"; 
       result = string.Format(format, decimalWhole);
     }
*/
           
  return result;
  }

UPDATE 3

After further testing... the "displayed value" is now correct, but the "bound property" is not.

the property TemplateNumber in the xaml code below has the value entered into the text box, not the value displayed by the text box (as modified by the IValueConverter.

<TextBox
  Text="{Binding TemplateNumber, 
  Converter={converters:DecimalConverter Precision=4},
  ValidatesOnDataErrors=True, 
  NotifyOnValidationError=True, 
  UpdateSourceTrigger=PropertyChanged, 
  Delay=500}"
  />

that is:

  • Enter value: 1.99999 (5 digits of precision)
  • Display value: 1.9999 (4 digits of precision, from IValueConverter)
  • TemplateNumber value: 1.99999 (5 digits of precision, unchanged from the value entered)

need to fix the rounding on the convert back too....

public object ConvertBack(object value, Type targetType, object 
   parameter, CultureInfo culture)
{
   // note: remove any extra white space
   if (decimal.TryParse(value?.ToString().Replace(" ",""), out decimal d))
   {
       int decimalPlaces = Precision ?? 0;
       decimal result = Math.Round(d, decimalPlaces, MidpointRounding.ToZero);
        return result;
   }
   return null;  // type is decimal? so null is legit
}

UPDATE 4

added Precision as a property to remove hard coded value. used MarkupExtension approach. this is really simple to add. cound not figure out how to add multiple properties (xaml barfs: future issue to resolve).

FYI the MarkupExtension to add Precision.

public class DecimalConverter : MarkupExtension, IValueConverter
{
  #region Markup Extension

  public int? Precision { get; set; } 

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

  #endregion // markup extension
...

the xaml is as per update 3.


Solution

  • You can use IValueConverter for example:

    class DecimalConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {                        
                return string.Format("{0:F3}", value);
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                if (decimal.TryParse(value?.ToString().Replace(" ", ""), out decimal num1))
                    return num1;
                return null;
            }
        }
    

    Add it to your control

    <UserControl.Resources>
        <local:DecimalConverter x:Key="MyDecimalConverter"/>
    </UserControl.Resources>
    

    Use in textbox

    <TextBox Text={Binding ...., Converter={StaticResource MyDecimalConverter}}/>