扩展内置类型的自定义格式功能

时间:2021-11-28 21:23:45

I have some rather awkward formatting requirements for decimal values. In a nutshell: display to two decimal places with a trailing space unless the third decimal is a 5, in which case display to three decimal places.

我对十进制值有一些相当尴尬的格式要求。简而言之:显示带有尾随空格的两个小数位,除非第三个小数是5,在这种情况下显示为三个小数位。

This formatting needs to be fairly flexible, too. Specifically, the trailing space will not always be desired, and a "½" may be preferred when the third decimal is a "5".

这种格式化也需要相当灵活。具体地,不总是期望尾随空间,并且当第三个小数是“5”时,“1/2”可能是优选的。

Examples:

例子:

  • 1.13 would be displayed as "01.13 " with a space or "01.13" without it
  • 1.13将显示为带有空格的“01.13”或没有它的“01.13”
  • 1.315 would be displayed as "01.315" or "01.31½"
  • 1.315将显示为“01.315”或“01.31½”

I need to use this logic consistently across otherwise unrelated pieces of UI. I have temporarily written it as a WPF value converter, but this is just for demonstration:

我需要在其他不相关的UI部分中使用此逻辑。我暂时将其写为WPF值转换器,但这仅用于演示:

public sealed class PriceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is decimal))
        {
            return DependencyProperty.UnsetValue;
        }

        var decimalValue = (decimal)value;
        var formattedDecimalValue = decimalValue.ToString("#0.000", CultureInfo.InvariantCulture);
        var lastFormattedChar = formattedDecimalValue[formattedDecimalValue.Length - 1];

        switch (lastFormattedChar)
        {
            case '0':
                return formattedDecimalValue.Substring(0, formattedDecimalValue.Length - 1) + " ";
            case '5':
                return formattedDecimalValue.Substring(0, formattedDecimalValue.Length - 1) + "½";
            default:
                return formattedDecimalValue;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

I am now trying to extract this into a more fundamental building block I can use throughout my UI layer. My initial thought was a custom format provider which I could then use from a Binding:

我现在正试图将其提取到一个更基本的构建块中,我可以在整个UI层中使用它。我最初的想法是一个自定义格式提供程序,然后我可以从绑定中使用它:

<TextBlock Text="{Binding Value, FormatString=WHATEVER}"/>

The idea is that format string could be something like "#0.005" which indicates to only show the third decimal place if it's a 5, or "#0.00F" which attempts to represent the third decimal as a fraction. However, I was unable to find a means of using a specific format provider from a binding, which seems like a major limitation to me, but maybe I'm missing something...?

我们的想法是格式字符串可能类似于“#0.005”,它表示只显示第三个小数位,如果它是5,或“#0.00F”,它试图将第三个小数表示为分数。但是,我无法从绑定中找到使用特定格式提供程序的方法,这似乎是对我的一个主要限制,但也许我错过了一些东西......?

After more experimentation and investigation, I came to the conclusion that my only option is to define my own type:

经过更多的实验和调查,我得出结论,我唯一的选择是定义我自己的类型:

public struct Price : IFormattable

This type would encapsulate the extra formatting capabilities I require. However, now I have another conundrum: in my ToString implementation, how can I leverage the existing formatting capabilities of decimal.ToString(string, IFormatProvider) without interfering with my own? It seems like this would be pretty darn messy, and it's causing me to lean towards a more limited solution of just defining "G" (two or three decimal places, no trailing space) and "S" (same as "G", but with trailing space if necessary) formats for my Price structure.

这种类型将封装我需要的额外格式化功能。但是,现在我有另一个难题:在我的ToString实现中,如何利用decimal.ToString(string,IFormatProvider)的现有格式化功能而不会干扰我自己的?这似乎是非常麻烦的,它让我倾向于更有限的解决方案,只是定义“G”(两位或三位小数,没有尾随空格)和“S”(与“G”相同,但是我的价格结构的格式。如果需要,有尾随空格)格式。

Can anyone tell me whether there's a way for me to do this kind of custom formatting capability without too much hassle?

任何人都可以告诉我是否有办法让我做这种自定义格式化功能没有太多麻烦?

2 个解决方案

#1


1  

See http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx for more details.

有关更多详细信息,请参阅http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx。

// "01.13 " or "01.13". Standard formatting applied: $123.45
// "01.315" or "01.31½". Standard formatting applied: $123.45

public class Test
{
    void Main()
    {
        decimal number1 = 1.13M;
        decimal number2 = 1.315M;

        string output1 = String.Format(new CustomNumberFormat(),
                                 "\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",
                                 number1, 123.45);
        Console.WriteLine(output1);

        string output2 = String.Format(new CustomNumberFormat(),
                                 "\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",
                                 number2, 123.45);
        Console.WriteLine(output2);
    }
}

public class CustomNumberFormat : System.IFormatProvider, System.ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter))
            return this;
        else
            return null;
    }

    public string Format(string fmt, object arg, System.IFormatProvider formatProvider)
    {
        // Provide default formatting if arg is not a decimal. 
        if (arg.GetType() != typeof(decimal))
            try
            {
                return HandleOtherFormats(fmt, arg);
            }
            catch (FormatException e)
            {
                throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);
            }

        // Provide default formatting for unsupported format strings. 
        string ufmt = fmt.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
        if (!(ufmt == "G" || ufmt == "S"))
            try
            {
                return HandleOtherFormats(fmt, arg);
            }
            catch (FormatException e)
            {
                throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);
            }

        // Convert argument to a string. 
        string result = ((decimal)arg).ToString("0#.000");

        if (ufmt == "G")
        {
            var lastFormattedChar = result[result.Length - 1];
            switch (lastFormattedChar)
            {
                case '0':
                    result = result.Substring(0, result.Length - 1) + " ";
                    break;
            }

            return result;
        }
        else if (ufmt == "S")
        {
            var lastFormattedChar = result[result.Length - 1];
            switch (lastFormattedChar)
            {
                case '0':
                    result = result.Substring(0, result.Length - 1);
                    break;
                case '5':
                    result = result.Substring(0, result.Length - 1) + "½";
                    break;
            }

            return result;
        }
        else
        {
            return result;
        }
    }

    private string HandleOtherFormats(string format, object arg)
    {
        if (arg is System.IFormattable)
            return ((System.IFormattable)arg).ToString(format, System.Globalization.CultureInfo.CurrentCulture);
        else if (arg != null)
            return arg.ToString();
        else
            return String.Empty;
    }
}

#2


0  

Try passing in your format provider as the parameter argument in your IValueConverter.Convert implementation:

尝试传递格式提供程序作为IValueConverter.Convert实现中的参数参数:

<TextBlock Text="{Binding Value, Mode=OneWay, Converter={StaticResource PriceConverter}, ConverterParameter=#0.00F"/>

Then, inside your converter:

然后,在您的转换器内:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    string formatString = parameter as string;

    if(formatString != null)
    {
        // Your code here
    }
    else
    {
        // Whatever you want to do here
    }

}

#1


1  

See http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx for more details.

有关更多详细信息,请参阅http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx。

// "01.13 " or "01.13". Standard formatting applied: $123.45
// "01.315" or "01.31½". Standard formatting applied: $123.45

public class Test
{
    void Main()
    {
        decimal number1 = 1.13M;
        decimal number2 = 1.315M;

        string output1 = String.Format(new CustomNumberFormat(),
                                 "\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",
                                 number1, 123.45);
        Console.WriteLine(output1);

        string output2 = String.Format(new CustomNumberFormat(),
                                 "\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",
                                 number2, 123.45);
        Console.WriteLine(output2);
    }
}

public class CustomNumberFormat : System.IFormatProvider, System.ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter))
            return this;
        else
            return null;
    }

    public string Format(string fmt, object arg, System.IFormatProvider formatProvider)
    {
        // Provide default formatting if arg is not a decimal. 
        if (arg.GetType() != typeof(decimal))
            try
            {
                return HandleOtherFormats(fmt, arg);
            }
            catch (FormatException e)
            {
                throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);
            }

        // Provide default formatting for unsupported format strings. 
        string ufmt = fmt.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
        if (!(ufmt == "G" || ufmt == "S"))
            try
            {
                return HandleOtherFormats(fmt, arg);
            }
            catch (FormatException e)
            {
                throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);
            }

        // Convert argument to a string. 
        string result = ((decimal)arg).ToString("0#.000");

        if (ufmt == "G")
        {
            var lastFormattedChar = result[result.Length - 1];
            switch (lastFormattedChar)
            {
                case '0':
                    result = result.Substring(0, result.Length - 1) + " ";
                    break;
            }

            return result;
        }
        else if (ufmt == "S")
        {
            var lastFormattedChar = result[result.Length - 1];
            switch (lastFormattedChar)
            {
                case '0':
                    result = result.Substring(0, result.Length - 1);
                    break;
                case '5':
                    result = result.Substring(0, result.Length - 1) + "½";
                    break;
            }

            return result;
        }
        else
        {
            return result;
        }
    }

    private string HandleOtherFormats(string format, object arg)
    {
        if (arg is System.IFormattable)
            return ((System.IFormattable)arg).ToString(format, System.Globalization.CultureInfo.CurrentCulture);
        else if (arg != null)
            return arg.ToString();
        else
            return String.Empty;
    }
}

#2


0  

Try passing in your format provider as the parameter argument in your IValueConverter.Convert implementation:

尝试传递格式提供程序作为IValueConverter.Convert实现中的参数参数:

<TextBlock Text="{Binding Value, Mode=OneWay, Converter={StaticResource PriceConverter}, ConverterParameter=#0.00F"/>

Then, inside your converter:

然后,在您的转换器内:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    string formatString = parameter as string;

    if(formatString != null)
    {
        // Your code here
    }
    else
    {
        // Whatever you want to do here
    }

}