WPF – formatowanie danych oraz konwersja typów

2012-12-09

W poprzednim wpisie omówiłem kilka mechanizmów walidacji wprowadzanych danych jakie oferuje technologia WPF. Dzisiaj rozszerzę ten temat o kwestie związane z formatowaniem danych oraz konwersją typów. Często zdarza się sytuacja, w której prezentując użytkownikowi pewne dane chcemy określić dla nich własny format. Równie często konieczne jest skonwertowanie wartości wprowadzonej przez użytkownika na docelowy typ danych. Operacje te nie powinny być implementowane po stronie źródła danych ponieważ ściśle dotyczą warstwy prezentacyjnej aplikacji. WPF oferuje kilka ciekawych rozwiązań w tym zakresie: StringFormat, ValueConverter oraz MultiValueConverter.
Na początku przygotujmy projekt zawierający ViewModel z czterema właściwościami oraz powiązany z nim widok (View):

ViewModel:

namespace WPFConverter
{
    public class ConverterViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        virtual protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private string firstName;
        private string lastName;
        private DateTime employmentData;
        private decimal salary;

        public string FirstName
        {
            get { return firstName; }
            set
            {
                firstName = value;
                OnPropertyChanged("FirstName");
            }
        }

        public string LastName
        {
            get { return lastName; }
            set
            {
                lastName = value;
                OnPropertyChanged("LastName");
            }
        }

        public DateTime EmploymentData
        {
            get { return employmentData; }
            set
            {
                employmentData = value;
                OnPropertyChanged("EmploymentData");
            }
        }

        public decimal Salary
        {
            get { return salary; }
            set
            {
                salary = value;
                OnPropertyChanged("Salary");
            }
        }
    }
}

View:

<Window x:Class="WPFConverter.ConverterView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WPFConverter"
        Title="View" Height="214" Width="350" WindowStartupLocation="CenterScreen">
    <Window.DataContext>
        <l:ConverterViewModel
            FirstName="Jan"
            LastName="Kowalski"
            EmploymentData="2010-12-01"
            Salary="18500.5" />
    </Window.DataContext>
    <Grid Margin="0,0,2,0">
        <Label Content="Imię i nazwisko:" HorizontalAlignment="Left"
               VerticalAlignment="Top" Margin="46,32,0,0"/>
        <Label Content="Data zatrudnienia:" HorizontalAlignment="Left"
               VerticalAlignment="Top" Margin="33,76,0,0"/>
        <Label Content="Wynagrodzenie:" HorizontalAlignment="Left"
               VerticalAlignment="Top" Margin="43,116,0,0"/>
        <TextBox Height="23" HorizontalAlignment="Left" Margin="139,35,0,0"
                 Name="tbName" VerticalAlignment="Top" Width="155"
                 Text="{Binding LastName}" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="139,79,0,0"
                 Name="tbEmploymentData" VerticalAlignment="Top" Width="155"
                 Text="{Binding EmploymentData}" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="139,119,0,0"
                 Name="tbSalary" VerticalAlignment="Top" Width="93"
                 Text="{Binding Salary}" />
    </Grid>
</Window>

Convert1

Od razu widać, że sposób prezentacji danych odbiega od naszych oczekiwań. W pierwszym polu powinny wyświetlać się imię i nazwisko, data powinna być sformatowana do postaci np. RRRR-MM-DD, kwota również mogłaby wyglądać lepiej.

StringFormat

Zacznijmy od najprostszego rozwiązania czyli sformatowania kwoty przy użyciu właściwości StringFormat obiektu wiązania (Binding). Właściwość ta pozwala zdefiniować format w jakim wyświetlana będzie określona wartość. Przypisujemy do niej wyrażenie w postaci {0:X}, gdzie 0 oznacza numer argumentu, X – format dla niego:

<TextBox Height="23" HorizontalAlignment="Left" Margin="139,119,0,0"
         Name="tbSalary" VerticalAlignment="Top" Width="93"
         Text="{Binding Salary, StringFormat={}{0:N2}}" />

Dla kwoty użyty został format N2, więcej o formatowaniu na MSDN: Formatting Types

Użycie na początku {} jest wymagane jedynie w przypadku, gdy wyrażenie rozpoczyna się od znaku {.

Jeżeli chcemy wymusić formatowanie zgodne z polskimi ustawieniami wystarczy w oknie widoku dodać parametr xml:lang=”pl-PL” (można go stosować także w poszczególnych elementach).

ValueConverter

ValueConverter jest znacznie bardziej rozbudowanym narzędziem. Pozwala na konwersję danych pomiędzy dwoma typami. Aby skorzystać z tego rozwiązania musimy stworzyć klasę implementującą interfejs IValueConverter. Klasa musi posiadać dwie metody: Convert oraz ConvertBack. Pierwsza z nich odpowiada za konwersję z typu źródłowego na docelowy, druga za operację odwrotną. Metody te posiadają parametry, w których przekazywane są: wartość do konwersji, docelowy typ danych, własny parametr oraz obiekt CultureInfo. Dobrą praktyką jest oznaczenie takiej klasy atrybutem ValueConversion zawierającym informacje o typach na jakich klasa operuje (typ źródłowy, typ docelowy).
Powyższe rozwiązanie zastosujemy do pola z datą. Oto utworzona klasa DateToStringConverter:

[ValueConversion(typeof(DateTime), typeof(string))]
public class DateToStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        DateTime date = (DateTime)value;
        string format = parameter.ToString();
        return date.ToString(format);
    }

    public object ConvertBack(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        DateTime result;
        if (DateTime.TryParse(value.ToString(), out result))
            return result;
        else
            return value;
    }
}

Klasa odpowiada za konwersję pomiędzy typem DateTime (właściwość ViewModel) a typem string (kontrolka widoku). W przypadku wyświetlania danych w widoku wywoływana będzie metoda Convert, do której przekazujemy parametr z formatem używanym podczas konwersji z DateTime na string. Gdy użytkownik wprowadzi wartość w kontrolce wywołana zostanie metoda ConvertBack konwertująca wartość string na DateTime.
Utworzenie obiektu DateToStringConverter w zasobach okna i użycie go w kontrolce:

<Window.Resources>
    <l:DateToStringConverter x:Key="DateToStringConverter" />
</Window.Resources>
<TextBox Height="23" HorizontalAlignment="Left" Margin="139,79,0,0"
         Name="tbEmploymentData" VerticalAlignment="Top" Width="155"
         Text="{Binding EmploymentData,
            Converter={StaticResource DateToStringConverter},
            ConverterParameter=yyyy-MM-dd}" />

lub utworzenie bezpośrednio w kontrolce:

<TextBox Height="23" HorizontalAlignment="Left" Margin="139,79,0,0"
         Name="tbEmploymentData" VerticalAlignment="Top" Width="155">
    <TextBox.Text>
        <Binding Path="EmploymentData" ConverterParameter="yyyy-MM-dd">
            <Binding.Converter>
                <l:DateToStringConverter />
            </Binding.Converter>
        </Binding>
    </TextBox.Text>
</TextBox>

Użycie obiektu ValueConverter polega na przypisaniu go do właściwości Converter obiektu Binding. Dodatkowo poprzez właściwość ConverterParameter możemy określić wartość trzeciego argumentu metod Convert i ConvertBack.

MultiValueConverter

ValueConverter pozwalał na konwertowanie pojedynczych wartości, z kolei MultiValueConverter umożliwia nam łączenie wielu wartości w jedną oraz rozbijanie pojedynczej wartości na kilka. Aby użyć tego mechanizmu musimy stworzyć klasę implementującą interfejs IMultiValueConverter. Podobnie jak przy ValueConverter klasa ta posiada metody Convert oraz ConvertBack. Różnica w stosunku do ValueConverter polega na innych parametrach oraz wynikach tych metod. W przypadku Convert przyjmowana jest tablica wartości a w wyniku zwracana pojedyncza wartość. ConvertBack z kolei przyjmuje pojedynczą wartość i zwraca tablicę.
Tego rozwiązania użyjemy w celu połączenia imienia i nazwiska w jedną wartość przy prezentacji (oddzielone spacją) oraz do ponownego ich rozbicia po wprowadzeniu nowej wartości przez użytkownika. Oto definicja klasy NameConverter:

public class NameConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter,
        CultureInfo culture)
    {
        string firstName = values[0] as string;
        string lastName = values[1] as string;

        return String.Format("{0} {1}", firstName, lastName);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
        CultureInfo culture)
    {
        string[] results = new string[2];
        char[] separator = {' '};
        string[] splitValues = ((string)value).Split(separator,
            StringSplitOptions.RemoveEmptyEntries);

        if (splitValues.Length > 0)
            results[0] = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(splitValues[0]);
        if (splitValues.Length > 1)
            results[1] = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(splitValues[1]);

        return results;
    }
}

Utworzenie obiektu NameConverter w zasobach okna i użycie go w kontrolce:

<Window.Resources>
    <l:NameConverter x:Key="NameConverter" />
</Window.Resources>
<TextBox Height="23" HorizontalAlignment="Left" Margin="139,35,0,0"
         Name="tbName" VerticalAlignment="Top" Width="155">
    <TextBox.Text>
        <MultiBinding Converter="{StaticResource NameConverter}">
            <Binding Path="FirstName" />
            <Binding Path="LastName" />
        </MultiBinding>
    </TextBox.Text>
</TextBox>

Aby do danej właściwości podpiąć kilka źródeł musimy użyć obiektu MultiBinding zawierającego kolekcję wiązań oraz wskazać w nim obiekt MultiValueConverter odpowiedzialny za konwersję wartości. Kolejność poszczególnych obiektów Binding determinuje kolejność wartości w tablicach przekazywanych i zwracanych w metodach Convert i ConvertBack.

W celu samej prezentacji danych zamiast obiektu MultiValueConverter moglibyśmy w obiekcie MultiBinding zdefiniować następujące formatowanie: StringFormat=”{}{0} {1}”.

Oto efekt końcowy:

Convert2

Podsumowanie

Na zakończenie kilka uwag. Jeżeli zastosujemy jednocześnie Converter oraz StringFormat to w pierwszej kolejności wywołany będzie Converter, później StringFormat.
Jeżeli w danej kontrolce mamy zdefiniowaną własną regułę walidacji (obiekt ValidationRule) to w zależności od ustawienia jej właściwości ValidationStep różna będzie kolejność wywoływania walidatora i konwertera po zmianie wartości w kontrolce. Jeżeli właściwość będzie ustawiona na „RawProposedValue” to kolejność będzie następująca: Validation -> Converter -> Property. Jeżeli ustawimy „ConvertedProposedValue” to otrzymamy kolejność: Converter -> Validation -> Property.
Oprócz pokazanych możliwości deklarowania konwerterów (zasoby okna lub kontrolka) możemy zastosować dwa inne rozwiązania. Pierwszym z nich jest utworzenie zewnętrznego zasobu z konwerterami i podpięcie go do zasobów aplikacji:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:l="clr-namespace:WPFConverter">
    <l:DateToStringConverter x:Key="DateToStringConverter" />
    <l:NameConverter x:Key="NameConverter" />
</ResourceDictionary>

<Application x:Class="WPFValidation.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Converters.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Drugim sposobem jest utworzenie abstrakcyjnej klasy dziedziczącej po System.Windows.Markup.MarkupExtension, a następnie użycie jej jako klasy bazowej dla tworzonych konwerterów:

public abstract class BaseConverter : System.Windows.Markup.MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Takie rozwiązanie pozwoli na bezpośrednie używanie konwerterów w kodzie XAML:

Converter={l:DateToStringConverter}

Więcej informacji można znaleźć na MSDN: BindingBase.StringFormat Property, IValueConverter InterfaceIMultiValueConverter Interface, MultiBinding Class.

Reklamy

Posted on 2012-12-09, in .NET/C# and tagged , , , , , , , . Bookmark the permalink. Dodaj komentarz.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d blogerów lubi to: