czwartek, 31 marca 2011

Konwersja danych w Silverlight i WPF - ValueConverter i StringFormatting

W dzisiejszym poście poruszę temat sposobu prezentacji danych użytkownikowi - a właściwie formatowania danych. Poruszam temat z dwóch powodów. Pierwszym jest to, iż wiedza ta jest potrzebna aby zrozumieć przyszły artykuł (dla tych którzy jeszcze nie posiadają wiedzy o konwersjach). Drugim powodem jest to, iż wielu deweloperów stara się formatować dane w kolekcji przed pokazaniem ich użytkownikowi. W tym celu najczęściej przechodzi się po kolekcji i odpowiedniego modyfikuje dane pobrane z np. z bazy danych czy też pliku. W WPFie są dwa bardzo wygodne mechanizmy dzięki którym unikniemy takiego zachowania.
Poruszany temat możemy wykorzystać zarówno w przypadku Silverlight jak i WPF. W obu przypadkach cała procedura przebiega w taki sam sposób. Przykład dla odmiany pokażę w WPF jednak tak jak wcześniej pisałem w Silverlight wszystko przebiega w taki sam sposób.

Rozpoczniemy od napisania przykładowej aplikacji:

Aplikacja pobiera dane z bazy danych AdventureWorksLT którą można pobrać ze stron microsoftu jako przykłady baz danych wraz z testowymi danymi danymi.
Kod który jest wymagany aby utworzyć pokazaną aplikację:

Klasa Product:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WpfApplication1
{
    public class Product
    {
        public string Name { get; set; }
        public string Color { get; set; }
        public decimal StandardCost { get; set; }
        public DateTime SellStartDate { get; set; }
    }
}

Klasa DataAccess:



Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Data.SqlClient;

namespace WpfApplication1
{
    public class DatabaseAccess
    {
        public ObservableCollection<Product> GetProducts()
        {
            ObservableCollection<Product> products = new ObservableCollection<Product>();
            using (SqlConnection conn = new SqlConnection(@"Data Source=ACER5738;Initial Catalog=AdventureWorksLT;Integrated Security=True"))
            {
                using(SqlCommand cmd = new SqlCommand("SELECT name, color, standardCost, sellStartDate FROM salesLT.Product", conn))
                {
                    conn.Open();
                    using (SqlDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Product tmpProduct = new Product();
                            tmpProduct.Color = dr["Color"].ToString();
                            tmpProduct.Name = dr["Name"].ToString();
                            tmpProduct.SellStartDate = (DateTime)dr["SellStartDate"];
                            tmpProduct.StandardCost = (decimal)dr["StandardCost"];
                            products.Add(tmpProduct);
                        }
                    }
                }
            }

            return products;
        }
    }
}

Okno:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private DatabaseAccess dataAccess;

        public MainWindow()
        {
            InitializeComponent();
            dataAccess = new DatabaseAccess();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            dgridProducts.ItemsSource = dataAccess.GetProducts();
        }
    }
}

Warstwa prezentacji (XAML):



Code:
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="554" Width="996" Loaded="Window_Loaded">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2.4*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <DataGrid Name="dgridProducts">
            
        </DataGrid>
        
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="0.8*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Product Name:" />
            <TextBlock Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.Name}" />
            <TextBlock Grid.Row="1" Text="Color:" />
            <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.Color}" />
            <TextBlock Grid.Row="2" Text="Standard Cost:" />
            <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.StandardCost}" />
            <TextBlock Grid.Row="3" Text="Sell Start Date:" />
            <TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.SellStartDate}" />
        </Grid>
    </Grid>
</Window>

Tak więc podstawowe "klocki" tworzące naszą aplikację są gotowe. Co w tej aplikacji nie podoba się na pierwszy rzut oka? Kilka rzeczy:
- design
- Standard Cost - 4 miejsca po przecinku zamiast standardowo dwóch
- Sell Stard Date - źle sformatowana data - mało czytelne
- nazwy kolumn

W tym poście zajmiemy się dwoma środkowymi problemami.
Aplikacja, którą napisaliśmy korzysta ze standardowego schematu:
1. Pobierz dane z bazy
2. Wyświetl dane użytkownikowi.

Taki schemat ma w domyśle niejawnie zapisane - dane są prawidłowe oraz są odpowiednio sformatowane. Oczywiście rzadko zdarza się pracować na idealnych danych. Dlatego aby poradzić sobie z sytuacjami, w których musimy dokonać obróbki wyświetlanych danych, WPF wprowadza nowe narzędzia w porównaniu do WindowsForms:
- string formatting - bardzo prosta a zarazem użyteczna właściwość pod nazwą Binding.StringFormat. Pozwala na zdecydowanie w jaki sposób będą wyświetlane ciągi znakowe.
- value converters - pozwala na dowolną konwersję między różnymi typami danych i zwracanie ich do bindowanej kontrolki

String Formatting
Pierwszy omawiany typ formatowania (string formatting) pozwala w łatwy sposób modyfikować łańcuchy tekstowe. Ustawienie właściwości Binding.StringFormat można zdefiniować do instrukcji:
{0:X} - gdzie 0 to pierwszy argument a X to format który chcemy zaaplikować dla danego tekstu.
W naszym pierwszym przypadku, czyli wyświetlania waluty możemy zastosować format:
{0:C} - gdzie C to standardowy sposób wyświetlania waluty w danym regionie. Wynik będzie następujący:

Kod formatujący dane wygląda następująco:


Code:
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.StandardCost, StringFormat={}{0:C}}" />

Dodając jedną właściwość, uzyskaliśmy pożądany wygląd. Warto zrwrócić na zastosowanie pustych nawiasów {}. Taki zabieg wymagany jest tylko w wypadku, gdy wartość właściwości StringFormat zamierzamy rozpocząć od nawiasu klamrowego. W innym przypadku np. StringFormat=Value is {0:C} - nawiasy nie są potrzebne.
Inne przydatne sposoby formatowania:
P - procenty
F[n] - formatuje liczbę rzeczywistą, zaokrąglając ją do n znaków po przecinku
d - krótka data
D - długa data


Value converter
Value converter to dużo bardziej zaawansowane narzędzie do konwersji typów. Pozwala na skonwertowanie jednego typu do dowolnego innego. Przykładów gdzie można użyć takiego zabiegu można sporo znaleźć w codziennej pracy:
- konwersja danych do postaci znakowej
- tworzenie specyfinczego typu dla WPF (np. z danych w postaci binarnej tworzyć obraz)
- zmiana właściwości w zależności od konwertowanej wartości - np. podświetlając na czerwono komórkę w wypadku błędnych danych.
Kroki które należy przejść aby skorzystać z dobrodziejstwa tej technologi są następujące:
1. Tworzymy klasę implementującą interfejs IValueConverter.
2. Dodajemy do stworzonej klasy atrybut ValueConversion, mówiący o typie z którego jest konwertowana wartość na docelowy typ.
3. Implementujemy metodę Convert, która zmienia typ wejściowy na żądany typ do wyświetlenia.
4. Implementujemy metodę ConvertBack, która definiuje jak ma zostać zmieniona wartość w drugą stronę - np. jak ma zostać zapisana do bazy danych

Zobaczmy więc przykład związany z konwersją daty.
Na początek klasa konwertujące, stworzona według podanych wyżej zasad:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;

namespace WpfApplication1
{
    [ValueConversion(typeof(DateTime), typeof(string))]
    class DataConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.GlobalizationCultureInfo culture)
        {
            DateTime dateTime = (DateTime)value;
            return string.Format("{0:MM/dd/yy}", dateTime);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.GlobalizationCultureInfo culture)
        {
            string date = value.ToString();
            DateTime result;
            if (DateTime.TryParse(date, out result))
            {
                return result;
            }
            return value;
        }
    }
}

Samo użycie sprowadza się do stworzenia obiektu konwertera i przypisaniu go do odpowiedniej właściwości podczas bindowania:


Code:
<Window.Resources>
        <local:DataConverter x:Key="DataConverter" />
    </Window.Resources>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.SellStartDate, Converter={StaticResource ResourceKey=DataConverter}}"  />

Efekt ostateczny:



W tym przypadku skorzystaliśmy z prostego zamienienia daty z jednego formatu na inny. Jednak w codziennej pracy można zetknąć się z innymi ciekawymi zagadnieniami:
- pobranie danych z bazy w formacie binarny i zamiana ich na obrazek
- zmiana koloru komórki w gridzie, elementu listy itp. na podstawie aktualnej wartości

Brak komentarzy:

Prześlij komentarz