Подтвердить что ты не робот

Расширение пользовательских возможностей форматирования встроенных типов

У меня есть некоторые довольно неудобные требования к форматированию для значений decimal. В двух словах: отображение до двух знаков после запятой с конечным пространством, если третья десятичная цифра не равна 5, и в этом случае отображается три знака после запятой.

Это форматирование также должно быть достаточно гибким. В частности, конечное пространство не всегда будет желательным, а "½" может быть предпочтительным, если третья десятичная цифра равна "5".

Примеры:

  • 1.13 будет отображаться как "01.13" с пробелом или "01.13" без него
  • 1.315 будет отображаться как "01.315" или "01.31½"

Мне нужно последовательно использовать эту логику для других не связанных между собой частей пользовательского интерфейса. Я временно написал его как конвертер значений 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();
    }
}

Теперь я пытаюсь извлечь это в более фундаментальный строительный блок, который я могу использовать во всем слое пользовательского интерфейса. Моя первоначальная мысль была поставщиком произвольного формата, который я мог бы использовать с Binding:

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

Идея заключается в том, что строка формата может быть чем-то вроде "# 0.005", что указывает только на отображение третьего десятичного знака, если оно равно 5 или "# 0.00F", которое пытается представить третью десятичную дробь в виде дроби. Однако мне не удалось найти способ использования определенного поставщика форматов из привязки, который кажется для меня основным ограничением, но, возможно, мне что-то не хватает...?

После дополнительных экспериментов и исследований я пришел к выводу, что мой единственный вариант - определить мой собственный тип:

public struct Price : IFormattable

Этот тип будет инкапсулировать дополнительные возможности форматирования, которые мне нужны. Однако теперь у меня есть еще одна загадка: в моей реализации ToString, как я могу использовать существующие возможности форматирования decimal.ToString(string, IFormatProvider), не вмешиваясь в мои собственные? Похоже, что это было бы довольно грязно, и это заставило меня склониться к более ограниченному решению только определения "G" (два или три десятичных знака, без конечного пространства) и "S" (то же, что и "G", но с конечным пространством, если необходимо) форматов для моей структуры Price.

Может ли кто-нибудь сказать мне, есть ли способ для меня сделать такую ​​возможность форматирования без лишних хлопот?

4b9b3361

Ответ 1

Подробнее см. 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

Попробуйте передать поставщик вашего формата в качестве аргумента parameter в вашей реализации IValueConverter.Convert:

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

Затем внутри конвертера:

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
    }

}