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

wtorek, 29 marca 2011

Obsługa strony z pytaniami - Radio Buttons w ASP MVC 2

Dzisiaj trochę o stworzeniu w MVC 2 kontrolera obsługującego przyciski typu radio.
Przyciski radio button to nic innego jak przyciski wyboru. Na wielu stronach można spotkać się z listami wyboru typu: kobieta/mężczyzna. Przykład? Ot chociażby zakładanie konta pocztowego w portalu onet:


W swoim ostatnim projekcie w MVC potrzebowałem całego formularza takich kontrolek. Z pozoru zadanie było bardzo proste i sprowadzało się do:
"Potrzebna jest formatka, na której wyświetlą się przyciski TAK/NIE dla listy pytań". Tak więc należało stworzyć mechanizm, który pobiera pytania z bazy danych, tworzy listę do wyboru i zapisuje do bazy danych rezultat.
Proste i nieskomplikowane zadanie na pierwszy rzut oka okazało się nie być takie oczywiste w późniejszym etapie i kolejnych życzeniach klienta.

Oczywiście w programowaniu, tak jak i matematyce, problem można rozwiązać na wiele sposobów. Ja przedstawię tutaj dwa sposoby, które sprawdzają się w moich projektach. Pierwszy przedstawiony sposób jest bardzo podobny do tego stosowanego w PHP (bezpośrednie odwołanie się do konkretnych pól formularza), drugi używa mechanizmu bindowania wbudowanego w MVC.

Sposób I

A więc rozpoczynamy, na potrzeby tego artykułu stworzymy plik XML z pytaniami o bardzo uproszczonym schemacie. Tworzymy więc na początek prosty plik XML Questions.XML o zawartości:



<?xml version="1.0" encoding="utf-8" ?>
<Questions>
  <Question>
    <Id>1</Id>
    <Content>Czy podoba Ci się ten blog?</Content>
  </Question>
  <Question>
    <Id>2</Id>
    <Content>Lubisz swoją uczelnie?</Content>
  </Question>
  <Question>
    <Id>3</Id>
    <Content>Mieszkasz w fajnej dzielnicy?</Content>
  </Question>
  <Question>
    <Id>4</Id>
    <Content>Chodzisz do kina?</Content>
  </Question>
  <Question>
    <Id>5</Id>
    <Content>Uprawiasz sport?</Content>
  </Question>
</Questions>


Każde pytanie ma swoją treść (content) oraz niepowtarzalny identyfikator (id). Tworzymy więc bardzo prosty model naszego pytania:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace RadioButtonsExample.Models
{
    public class Question
    {
        public int Id { get; set; }
        public string Content { get; set; }
    }
}

Następnie tworzymy kontroler - Home. W metodzie Index wprowadzimy następujący kod:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Xml.Linq;
using RadioButtonsExample.Models;

namespace RadioButtonsExample.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            XDocument xmlQuestionDocument = XDocument.Load("Questions.xml");
            IList<Question> lQuestions = new List<Question>();
            var questions = from a in xmlQuestionDocument.Elements()
                            select a;
            foreach (var item in questions.Elements("Questions.xml"))
            {
                lQuestions.Add(new Question { Id = int.Parse(item.Element("Id").Value), Content = item.Element("Content").Value });
            }

            return View(lQuestions);
        }

    }
}

Tak więc do widoku zostanie przekazana kolekcja pytań. Widok tworzymy oczywiście silnie typowany:


Code:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<IEnumerable<RadioButtonsExample.Models.Question>>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Index</title>
</head>
<body>
    <div>
    <% using (Html.BeginForm())
       { %>
       <% foreach (var item in Model)
          { %>
          <%: item.Id.ToString() + ". " + item.Content %>
          <br />
          <%: Html.RadioButton("P" + item.Id.ToString(), "T", true) %> TAK<br />
          <%: Html.RadioButton("P" + item.Id.ToString(), "N", false) %> NIE<br />
          <br />
       <% } %>
       <br />
       <br />
       <input type="submit" value="Wyślij" />
    <% } %>
    </div>
</body>
</html>

ASP.MVC posiada bardzo silny model bindingu. W łatwy sposób możemy bindować całe obiekty do formularzy i je zwracać w postaci obiektu. W tym przypadku, łatwiej jest skorzystać z kolekcji - FormCollection. Zawiera ona klucze i wartości pól na formularzu.

Wyświetlanie: zakładamy na początku najprostszą wersję:
- Wszystkie pytania mają domyślnie zaznaczoną odpowiedź.
W tym przypadku odbieramy rezultat jako kolekcję kluczy formularza. Ważne jest tu słowo kluczy - które jest utożsamione z nazwą konkretnego pola na formularzu. Pola na formularzu są budowane w sposób następujący: P{id} czyli dla pytania pierwszego mamy P1, drugiego P2 itd.
W przypadku gdy nie wybierzemy zaznaczenia domyślnego dla jednej z odpowiedzi, a użytkownik nie zaznaczy żadnej z odpowiedzi w kolekcji kluczy nie znajdziemy interesującej nas pozycji. W takiej sytuacji pozostaje sprawdzenie czy kolekcja zawiera żądaną przez nas wartość. Ja w tym celu postanowiłem stworzyć dodatkową klasę, która tworzy słownik z kontrolkami które są na formularzu i pozwala w bardzo szybki sposób sprawdzić, czy żądany klucz jest w kolekcji, czy też nie:



Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace RadioButtonsExample.Models
{
    public class Helpers
    {
        public Dictionary<string, string> GetKeyValueFormControls(FormCollection formCollection)
        {
            Dictionary<string, string> result = new Dictionary<string, string>();
            if (formCollection != null && formCollection.Count > 0)
            {
                for (int i = 0; i < formCollection.AllKeys.Length; i++)
                {
                    result.Add(formCollection.AllKeys[i], formCollection[formCollection.AllKeys[i]]);
                }
            }

            return result;
        }
    }
}


Sposób II

Tym razem wykorzystamy możliwości bindingu wbudowane w MVC 2. Większość baz na rynku (zwłaszcza darmowych) nie posiada specjalnego pola pozwalającego zakwalifikować odpowiedź jako typ logiczny. Dwoma najprostszymi więc sposobami na uzyskanie zapisania do bazy odpowiedniej wartości takiego pola jest użycie zmiennej tekstowej typu CHAR(1) - reprezentującej zamknięty zbiór odpowiedzi {T,N} bądź też typ całkowity INTEGER o wartościach ze zbioru {0,1}. W moim przypadku wybiorę typ tekstowy.
Aby dodatkowo uprościć całość do klasy dołożę jedno dodatkowe pole, określające odpowiedź na zadane pytanie, oprócz tego utworzymy klasę Ankieta zawierającą listę pytań (tym razem w narodowym języku):



public class Pytanie
    {
        public int Id { get; set; }
        public string Tresc { get; set; }
        public string Odpowiedz { get; set; }
    }



using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication1.Models
{
    public class Ankieta
    {
        public List<Pytanie> Pytania { get; set; }
    }
}

Dzięki temu będzie można bezpośrednio z formularza zwrócić odpowiednią wartość. Oczywiście można by stworzyć oddzielną klasę na odpowiedź, w tym prostym przypadku nie skorzystam z tej możliwości.
Przejdźmy do konstrukcji widoku dla takiego modelu. Tutaj także posłużę się dwoma przykładami widoków. Pierwszy, aby pokazać co tak naprawdę dzieje się podczas bindowania kolekcji w MVC a drugi - oczywiście upraszczający całość:
1. Sposób trudniejszy, czyli co w środku piszczy:



Code:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Ankieta>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Index</title>
</head>
<body>
    <div>
        <% using (Html.BeginForm())
           { %>
           <%  for(int i = 0; i < Model.Pytania.Count; i++)
              { %>
              <%: Html.Hidden(string.Format("Pytania[{0}].ID", i), Model.Pytania[i].Id) %>
              <%: Html.DisplayFor(x => x.Pytania[i  ].Tresc) %>
              <br />
              <%: Html.RadioButton(string.Format("Pytania[{0}].Odpowiedz", i), "T") %>TAK
              <br />
              <%: Html.RadioButton(string.Format("Pytania[{0}].Odpowiedz", i), "N") %>NIE
              <br />
              <br />
           <% } %>

           <input type="submit" value="Wyślij" />
        <% } %>
    </div>
</body>
</html>

Sprawdźmy od razu co generuje ten kod w HTML:



Code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>
 Index
</title></head>
<body>
    <div>
        <form action="/" method="post"><input id="Pytania_0__ID" name="Pytania[0].ID" type="hidden" value="1" />
              Czy lubisz te strone?
              <br />
              <input id="Pytania_0__Odpowiedz" name="Pytania[0].Odpowiedz" type="radio" value="T" />TAK
              <br />
              <input id="Pytania_0__Odpowiedz" name="Pytania[0].Odpowiedz" type="radio" value="N" />NIE
              <br />
              <br />
           <input id="Pytania_1__ID" name="Pytania[1].ID" type="hidden" value="2" />
              Czy chcialbys pomoz tworzyc te strone?
              <br />
              <input id="Pytania_1__Odpowiedz" name="Pytania[1].Odpowiedz" type="radio" value="T" />TAK
              <br />
              <input id="Pytania_1__Odpowiedz" name="Pytania[1].Odpowiedz" type="radio" value="N" />NIE
              <br />
              <br />
           

           <input type="submit" value="Wyślij" />
        </form>
    </div>
</body>
</html>

Zwróćmy uwagę na tworzenie nazwy kontrolki. Mechanizm Bindingu w MVC nie tylko musi znać dokładną nazwę pola do którego binduje dany element strony, ale także pozycję w kolekcji. Dlatego też aby zwrócić poprawnie dane z formularza jako obiekt Ankieta:


Code:
<input id="Pytania_1__Odpowiedz" name="Pytania[1].Odpowiedz" type="radio" value="T" />TAK

Sama nazwa generowana jest w prosty sposób:


Code:
<%: Html.RadioButton(string.Format("Pytania[{0}].Odpowiedz", i), "T") %>TAK

Co prawda nie jest problemem pisanie takiego kodu, jednak po pewnym czasie staje się on mało czytelny (jak również dla kolejnych programistów tego projektu z pewnością nie będzie wygodnie domyślać się o co chodziło autorowi). Można całość uprościć do wyrażenia lambda:


Code:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Ankieta>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Index</title>
</head>
<body>
    <div>
        <% using (Html.BeginForm())
           { %>
           <%  for(int i = 0; i < Model.Pytania.Count; i++)
              { %>
              <%: Html.HiddenFor(x => x.Pytania[i].Id) %>.
              <%: Html.DisplayFor(x => x.Pytania[i].Tresc) %>
              <br />
              <%: Html.RadioButtonFor(x => Model.Pytania[i].Odpowiedz, "T")  %>TAK
              <br />
              <%: Html.RadioButtonFor(x => Model.Pytania[i].Odpowiedz, "N") %>NIE
              <br />
              <br />
           <% } %>

           <input type="submit" value="Wyślij" />
        <% } %>
    </div>
</body>
</html>

Oczywiście wygenerowany kawałek kodu HTML nie będzie się niczym różnił od poprzedniego, ale nie zawiera elementów gimnastyki dla zaawansowanych podczas tworzenia nazwy elementu HTML.

Sposób którzy obierzemy zależy oczywiście tylko od naszych preferencji. Ja przeważnie stosuje drugi, wykorzystujący binding. Uważam, że jest to bardziej poprawny sposób bindowania danych w frameworku, który właściwie słynie z łatwości bidningu całych obiektów do formularzy HTML.

Ktoś dociekliwy może posunąć się dalej i stworzyć własny HTML helper, generujący odpowienie bloki przycisków typu radio z odpowiednimi wartościami korzystając z list, typów wyliczeniowych itp. Ograniczeniem w tym przypadku jest tylko i wyłączenie wyobraźnia.

niedziela, 27 marca 2011

Aplikacja z wykorzystaniem SqLite oraz WPF

Jak we wcześniejszym poście obiecałem, zamieszczę aplikację wykonaną przy użyciu bazy SqLite oraz technologi WPF o której już sporo się przewinęło na tym blogu.

Zadaniem było stworzenie programu umożliwiającego gromadzenie linków do stron, podzielonych na kategorie według uznania użytkownika.

Rozpoczynamy od schematu bazy danych:




Jak widać, 3 tabele:
Url - opisuje link
Category - kategoria danego linka
UrlCategory - tabela techniczna - umożliwia przypięcie linka do wielu kategorii

Interfejs programu zostanie napisany przy użyciu WPF.

Cały program wygląda następująco:

Wykorzystałem komponent Ribbon - wstążka znany z Microsoft Office. Użytkownik ma możliwość wyszukiwania linków po opisie.
Dodawanie kategorii:


Dodawanie linku (wcześniej wybranie kategorii do której będzie należał link):






Implementacja:
Podczas tworzenia kodu programu nie używałem na sztywno providera. Tak więc zamiast SqLiteConnection użyłem DbConnection. Pozwala to na szybkie przełączanie się między różnymi bazami danych. Klient zmieniając tylko odpowiednio ConnectionString może użyć własnej bazy danych. Zobaczmy na przykład kodu napisanego właśnie w takim stylu:

        public IList<Category> GetAllCategories()
        {
            IList<Category> categories = new List<Category>();
            using (DbConnection dbconn = dbProvider.CreateConnection())
            {
                using (DbCommand dbcmd = dbProvider.CreateCommand())
                {
                    dbconn.ConnectionString = connectionString.ConnectionString;
                    dbcmd.Connection = dbconn;
                    dbcmd.CommandText = @"SELECT IdCategory, CategoryName
                                            FROM Category";
                    dbconn.Open();
                    using (DbDataReader dr = dbcmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Category tmp = new Category();
                            tmp.CategoryName = (string)dr["CategoryName"];
                            tmp.IdCategory = int.Parse(dr["IdCategory"].ToString());
                            categories.Add(tmp);
                        }
                    }
                }
            }
            return categories;
        }

W całej aplikacji parametry przekazywane są poprzez obiekt DbParameter, unikamy w ten sposób zawiłych łączeń kodu, jak i to co idzie do bazy nie zawiera niebezpieczeństwa SQL injection (wstrzyknięcie złośliwego kodu). Przykład takiego kodu:

       public IList<Url> GetUrlsByCategoryId(int idCategory)
        {
            IList<Url> urls = new List<Url>();
            using (DbConnection dbconn = dbProvider.CreateConnection())
            {
                using (DbCommand dbcmd = dbProvider.CreateCommand())
                {
                    dbcmd.Connection = dbconn;
                    dbconn.ConnectionString = connectionString.ConnectionString;
                    dbcmd.CommandText = @"SELECT u.IdUrl, Url,Description
                                            FROM Url u JOIN UrlCategory uc ON u.IdUrl = uc.IdUrl JOIN Category c ON uc.IdCategory = c.IdCategory
                                            WHERE c.IdCategory = @IdCategory";
                    DbParameter idCategoryParameter = dbcmd.CreateParameter();
                    idCategoryParameter.DbType = DbType.Int32;
                    idCategoryParameter.ParameterName = "@IdCategory";
                    idCategoryParameter.Value = idCategory;
                    dbcmd.Parameters.Add(idCategoryParameter);
                    dbconn.Open();
                    using (DbDataReader dr = dbcmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Url tmp = new Url();
                            tmp.IdUrl = int.Parse(dr["IdUrl"].ToString());
                            tmp.Description = (string)(dr["Description"] == DBNull.Value ? "" : dr["Description"]);
                            tmp.UrlString = (string)(dr["Url"] == DBNull.Value ? "" : dr["Url"]);
                            urls.Add(tmp);
                        }
                    }
                }
            }
            return urls;
        }


Wygląd elementów ListBox został zmieniony poprzez zastosowanie odpowiedniego stylu dla nich:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="lbxItemStyle" TargetType="{x:Type ListBoxItem}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
        <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
        <Setter Property="Padding" Value="2,0,0,0"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                    <Grid>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:1"/>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="contentPresenter">
                                            <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="border">
                                            <EasingDoubleKeyFrame KeyTime="0" Value="0.293"/>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Disabled"/>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Border x:Name="border" BorderBrush="#FF93A3E5" BorderThickness="2" Margin="0" CornerRadius="10">
                            <Border.Background>
                                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                    <GradientStop Color="Black"/>
                                    <GradientStop Color="#FF92A2CA" Offset="1"/>
                                </LinearGradientBrush>
                            </Border.Background>
                            <ContentPresenter x:Name="contentPresenter" HorizontalAlignment="Center" VerticalAlignment="Center">
                                <ContentPresenter.OpacityMask>
                                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                        <GradientStop Color="Black" Offset="0"/>
                                        <GradientStop Color="White" Offset="1"/>
                                    </LinearGradientBrush>
                                </ContentPresenter.OpacityMask>
                            </ContentPresenter>
                        </Border>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="true">
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                        </Trigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsSelected" Value="true"/>
                                <Condition Property="Selector.IsSelectionActive" Value="false"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                        </MultiTrigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- Resource dictionary entries should be defined here. -->
</ResourceDictionary>


Dodatkowe uwagi:
Należy pamiętać, że korzystając z providera który nie jest dostępny domyślnie na platformie .NET należy dodać go do odpowiedniej sekcji w pliku App.config bądź też bezpośrednio zmodyfikować plik Machine.config na maszynie klienta. W obu rozwiązaniach należy dodać następujący wpis:

  <system.data>
    <DbProviderFactories>
      <add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".Net Framework Data Provider for SQLite"
           type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
    </DbProviderFactories>
  </system.data>


Kolejnym ważnym punktem jest udostępnienie wraz z exe aplikacji odpowiedniej biblioteki umożliwiającej korzystanie z SqLite:



Co brakuje w projekcie a przydałoby się dorobić? Oczywiście jest cała lista takich rzeczy (które są na tzw. liście do dokończenia kiedyś :) ). Projekcik miał za zadanie pokazać jak posługiwać się bazą SqLite brakuje więc w nim kilku moim zdaniem przydatnych rzeczy:
- możliwosć dodawania do wielu kategorii
- skórki dla grida
- większa obsługa klawiatury w aplikacji (szybciej coś wyklikać na klawiaturze niż namierzyć myszką :) )
- logowanie do aplikacji
- eksport linków do pliku itp.
- poprawienie wizualnego wyglądu aplikacji poprzez zastosowanie motywów (kiedyś opiszę jak je wykorzystuję w aplikacji Silverlight którą ostatnio tworzyłem)

Projekt w obecnym stanie można pobrać i przetestować stąd.
Życzę udanego czytania kodu jak i eksperymentowania poprzez dodawanie własnych modułów/rozszerzanie istniejącej funkcjonalności.

Oczywiście w razie pytań służę pomocą.