Artykuł

lut 17 2012
0

C#, WPF oraz SQLite razem w jednym projekcie

Tworząc aplikację, która operuje na jakichkolwiek danych, które mają być dostępne również w przyszłości, szybko dochodzimy do momentu, kiedy to musimy wybrać odpowiedni sposób składowania tych danych.

Po szybkim namyśle, przyjdzie Wam z pewnością kilka propozycji:

  • Tekstowy plik danych o określonej strukturze
  • Plik XML (można korzystać z serializacji)
  • Baza danych

Każde z tych rozwiązań ma swoje określone wady i zalety, a wszystko zależy również od skali projektu. W moim przypadku miałem właśnie do czynienia ostatnio raczej ze stosunkowo nie dużą aplikacją. Z miejsca odrzuciłem pierwsze dwa rozwiązania, ponieważ wymagają one raczej złożonego przetwarzania plików, a dodatkowo chciałem mieć łatwy dostęp do danych i łatwą możliwość zbindowania.

Idealnym rozwiązaniem wydawała się być baza danych, jednak wymagała by ona instalacji serwera baza danych, co przy skali projektu byłoby grą nie wartą świeczki - więc kolejne pudło.

Troszkę już zrezygnowany, przypomniałem sobie o SQLite, który w tym przypadku okazał się strzałem w dziesiątkę i rozwiązywał wszystkie moje problemy.

Co to jest SQLite?

SQLite to najprościej mówiąc system bazy danych, nie wymagający od użytkownika docelowego instalacji żadnej dodatkowej aplikacji, a zarazem umożliwiający wykonywanie zapytań SQL.

Cała baza danych SQLite umieszczona jest w jednym pliku, dzięki czemu w łatwy sposób możemy sobie taką bazę przenieść, czy też po prostu dołączyć do naszej aplikacji.

Obecnie bazy typu SQLite popularne są w rozwiązaniach mobilnych - szczególnie w Androidzie, gdzie są najlepszym sposobem przechowywania danych. Sprawdzić się mogą również w prostych zastosowaniach desktopowych i właśnie do tego celu wykorzystamy ją dziś.

Tworzenie bazy danych w SQLite Administrator

Zanim przystąpimy do kodowania, powinniśmy utworzyć bazę danych. Można w tym celu wykorzystać wiele różnych narzędzi, ale mi szczególnie do gustu przypadł SQLite Administrator.

Po pobraniu, wystarczy rozpakować archiwum ZIP i już można korzystać z aplikacji.

Po uruchomieniu, przywita nas prosty i przyjazny design. Aplikacja posiada również język polski. Jeśli nie będzie on włączony, to można to zrobić z poziomu menu Pomoc/Help.

Aby cokolwiek zrobić dalej, musimy utworzyć nową bazę danych oraz przynajmniej jedną tabelę. Naturalnie rozpoczniemy od utworzenia bazy danych. Można to zrobić za pomocą pierwszej ikonki w pasku narzędziowym, lub po prostu wybierając opcję Nowa z menu Baza danych. Proste - prawda:)?

Przy tworzeniu bazy danych, musimy wskazać jej lokalizację oraz nazwę. Ja swoją bazę nazwę Person (znów będziemy zajmować się danymi osobowymi:)

W momencie gdy już mamy bazę danych, możemy dodać tabelę. Tu zaskoczenia nie będzie, ponieważ wystarczy wybrać opcję Nowa z Tabela:)

Tworzenie tabeli, wymaga już nieco więcej zachodu, ponieważ musimy zdefiniować jej pola. Przykładową strukturę mojej tabeli, zamieściłem na screenie 2. Do niej, będę się odnosił w dalszej części tekstu.

Jeśli nie chce Wam się przepisywać pól zamieszczonych na screenie, możecie wykonać po prostu poniższy kod (menu Zapytania opcja Wykonaj bez wyniku), która doda Wam moją tabelę:

CREATE TABLE [PersonData] (
[PersonId] INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT,
[PersonName] VARCHAR(50)  NOT NULL,
[PersonLastName] VARCHAR(50)  NOT NULL,
[PersonAge] INTEGER(3)  NOT NULL
)

Kod tabeli, wygenerowałem sobie właśnie w SQLite Administratorze:)

Zasadniczo to już wszystko czego oczekiwaliśmy od tego narzędzia. Baza danych, domyślnie zostanie zapisana z kodowaniem UTF-8, więc nie powinno być żadnych problemów.

Jeśli będziecie chcieli jednak dodać dane za pomocą tej aplikacji, muszę Was uprzedzić, że mogą wystąpić problemy z polskimi znakami. Nie znam przyczyn dlaczego tak się dzieje, ale gdy dodawałem kilkukrotnie przykładowe dane INSERTem, to w mojej aplikacji pojawiły się krzaki dla tych rekordów (takiego problemu nie było gdy dodawałem polskie znakiem INSERTem z poziomu C#).

Pobranie oraz instalacja bibliotek

SQLite nie jest natywnie wspierany przez Visual Studio, ale istnieją na szczęście odpowiednie biblioteki, w które zaraz się zaopatrzymy:)

Musicie wejść pod ten adres. Następnie, odszukujecie interesujący Was framework oraz wersję systemu (32/64 bit). Oczywiście, jeśli macie system 64 bitowy, a chcecie utworzyć aplikację 32 bitową, to w takim przypadku musicie pobrać bibliotekę 32bit. Tak było właśnie w moim przypadku.

Pobieramy pierwszy link, z interesującej nas sekcji (w pierwszych czterech sekcjach, mamy gotowe instalatory - sugeruje skorzystanie właśnie z nich).

Po pobraniu, instalujemy uzyskaną paczkę. Od tego momentu, będziemy mogli już w normalny sposób podłączać bibliotekę do projektu.

Scenariusz testowy

Mamy już wszystkie potrzebne elementy w systemie, to teraz przydałoby się napisać jakąś specjalną testową aplikację. W tym celu, wykorzystamy WPF - a jakże!

Ewentualnie można skorzystać również z Windows Forms. Sama obsługa bazy danych będzie identyczna, odrobinę inaczej wygląda tylko bindowanie.

Generalnie skorzystamy z adaptera bazy danych, który w tym przypadku zawarty jest w klasie SQLiteDataAdapter. Dane z adaptera podepniemy do kontrolki ListView.

Pokażę Wam również w jaki sposób dodawać, edytować oraz usuwać dane w naszej bazie danych. Pominiemy już w tym przypadku jakieś wymyślne kreatory i okienka, lecz bardziej pokaże jak to zrobić na sucho, na zadeklarowanych w kodzie danych. W takiej sytuacji będzie Wam już bardzo łatwo dorobić odpowiednie interaktywne okienko, w którym będziecie mogli wprowadzać swoje dane.

Projekt w Visual Studio

Proces tworzenia, rozpoczniemy od utworzenia projektu WPF Application (jeśli macie z tym jakikolwiek problem, to zapraszam do zapoznania się z moim WPF Tutorialem).

Po utworzeniu projektu, możemy od razu dodać referencję do biblioteki SQLite, którą wcześniej zainstalowaliśmy. Referencję dodajemy w standardowy sposób. Na zakładce .NET musimy odszukać komponent o nazwie System.Data.SQLite Core i dodać do go do projektu.

Po dodaniu referencji, musimy dodać do projektu naszą świeżo utworzoną bazę, korzystać z opcji Add.. Existing item. Następnie, należy zmienić jego domyślne właściwości (okienko Properties w Visual Studio):

  • Build należy ustawić na Content
  • Copy action na Copy always ewentualnie Copy if newer

Dzięki temu ustawieniu, plik bazy danych zawsze znajdzie się w folderze docelowym po kompilacji:)

W kolejnym kroku, musimy stworzyć warstwę wizualną. Na nasze potrzeby, potrzebna będzie kontrolka ListView oraz trzy przyciski, które będą wyzwalać akcje (dodawanie/edycję/usuwanie). Zgodnie z tym co napisałem wcześniej, wszystkie wyżej wymienione akcje będą operować na danych umieszczonych w kodzie. Wiem, jest to skrajnie nieeleganckie rozwiązanie, ale wykorzystuje je tylko w celach demonstracyjnych;)

Wróćmy zatem do wyglądu. Poniższy listing dostarcza całą warstwę wizualna oraz podpina zdarzenia do wszystkich elementów. Na poniższym listingu zawarłem cały kod z mojego pliku XAML. Pamiętajcie, że użyłem tutaj mojej nazwy projektu i okna.

<Window x:Class="SQLiteExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Closing="Window_Closing">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView Grid.Row="0" Margin="5" 
            x:Name="lstItems" ItemsSource="{Binding}">
            <ListView.View>
                <GridView AllowsColumnReorder="True">
                    <GridViewColumn>
                        <GridViewColumn.HeaderTemplate>
                            <DataTemplate >
                                <TextBlock Text="Id" Margin="2" />
                            </DataTemplate>
                        </GridViewColumn.HeaderTemplate>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock Margin="2" 
                                        Text="{Binding Path=PersonId}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn>
                        <GridViewColumn.HeaderTemplate>
                            <DataTemplate>
                                <TextBlock Text="Imię" Margin="2" />
                            </DataTemplate>
                        </GridViewColumn.HeaderTemplate>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock Margin="2"
                                        Text="{Binding Path=PersonName}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn>
                        <GridViewColumn.HeaderTemplate>
                            <DataTemplate>
                                <TextBlock Text="Nazwisko" Margin="2" />
                            </DataTemplate>
                        </GridViewColumn.HeaderTemplate>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock Margin="2"
                                        Text="{Binding Path=PersonLastName}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn>
                        <GridViewColumn.HeaderTemplate>
                            <DataTemplate>
                                <TextBlock Text="Wiek" Margin="2" />
                            </DataTemplate>
                        </GridViewColumn.HeaderTemplate>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock Margin="2"
                                        Text="{Binding Path=PersonAge}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
        <WrapPanel Grid.Row="1">
            <Button Content="Dodaj" x:Name="btnAdd" Margin="5" 
                Click="btnAdd_Click" />
            <Button Content="Edytuj" x:Name="btnEdit" Margin="5" 
                Click="btnEdit_Click" />
            <Button Content="Usuń" x:Name="btnDelete" Margin="5" 
                Click="btnDelete_Click" />
        </WrapPanel>
    </Grid>
</Window>

Ponieważ wszystkimi rzeczami w powyższym listingu zajmowałem się już na blogu w tutorialu WPF, to teraz tylko zaakcentuje najważniejsze punkty.

Po pierwsze, w linii 11 wskazujemy na źródło danych poprzez bindowanie (kod dopełnimy w pliku code-behind).

W liniach 13-74 definiujemy kolumny, które pojawią się w tabeli oraz przypisujemy do nich odpowiednie kolumny z bazy danych (np. {Binding Path=PersonId}):

  • PersonId
  • PersonName
  • PersonLastName
  • PersonAge

Każdej z tych kolumn ustawiamy pewne właściwości wizualne. Oczywiście, można by tu jeszcze dodać jakiś uniwersalny styl itp. - to już zostawiam w Waszej gestii.

W liniach 77-84 ustawiamy trzy przyciski. Obsługę zdarzeń podepniemy oczywiście w pliku *.cs.

Skoro warstwę wizualną mamy za sobą, pora przejść do kodu C# w pliku *.cs. Na początku, zdefiniujemy pola klasy, związane z obiektami odpowiedzialnymi za bazę danych:

private SQLiteDataAdapter m_oDataAdapter = null;
private DataSet m_oDataSet = null;
private DataTable m_oDataTable = null;

Następnie stworzymy prywatną metodę, która będzie pobierać dane i inicjalizować DataAdapter:

private void InitBinding()
{
    SQLiteConnection oSQLiteConnection = 
        new SQLiteConnection("Data Source=Person.s3db");
    SQLiteCommand oCommand = oSQLiteConnection.CreateCommand();
    oCommand.CommandText = "SELECT * FROM PersonData";
    m_oDataAdapter = new SQLiteDataAdapter(oCommand.CommandText,
        oSQLiteConnection);
    SQLiteCommandBuilder oCommandBuilder = 
        new SQLiteCommandBuilder(m_oDataAdapter);
    m_oDataSet = new DataSet();
    m_oDataAdapter.Fill(m_oDataSet);
    m_oDataTable = m_oDataSet.Tables[0];
    lstItems.DataContext = m_oDataTable.DefaultView;
}

Spójrzmy zatem co tu się dzieje, a dzieje się wiele interesujących rzeczy:) Po pierwsze, w liniach 3-4 tworzymy obiekt SQLiteConnection. W tym celu, musimy zdefiniować ConnectionString, który w tym przypadku wskazuje plik bazy danych. Jeśli plik bazy umieścimy w głównym katalogu projektu, to taka właśnie konstrukcja wystarczy. Można oczywiście wskazać pełną ścieżkę do lokalizacji na dysku twardym.

Następnie w liniach 5-6 tworzymy komendę, która pobierze dane z naszej wcześniej zdefiniowanej tabeli.

W liniach 7-8, zdefiniujemy adapter danych, przekazując mu wcześniej zdefiniowaną komendę oraz uchwyt do połączenia z bazą danych.

W liniach 9-10, tworzymy obiekt klasy SQLiteCommandBuilder, która wygeneruje nam wszystkie komendy do operacji CRUD. Dzięki temu, wszystko będzie się działo automagicznie:)

W liniach 11-12, tworzymy obiekt DataSet i wypełniamy go danymi pozyskanymi przez adapter.

W linii 13, pozyskujemy tabelę bazy danych, po to by w linii 14, przypisać ją do kontekstu danych naszej listy utworzonej wcześniej w XAMLu.

Metodę InitBinding należy wywołać najlepiej już w konstruktorze. Tak zdefiniowana powinna już w tym momencie zasilać naszą listę danymi. Oczywiście odpowiednie dane muszą istnieć w bazie danych, o co zadbamy za chwilę;).

Po utworzeniu metody InitBinding, przed nami zostały do wypełnienia cztery metody, który utworzyliśmy przy okazji tworzenia zdarzeń w interfejsie użytkownika:

private void btnAdd_Click(object sender, RoutedEventArgs e)
{
}

private void btnEdit_Click(object sender, RoutedEventArgs e)
{
}

private void btnDelete_Click(object sender, RoutedEventArgs e)
{
}

private void Window_Closing(object sender, 
    System.ComponentModel.CancelEventArgs e)
{
}

Rozprawimy się z nimi teraz po kolei.

Dodawanie danych

Najpierw zajmiemy się dodawaniem danych, a jest za to odpowiedzialny przycisk btnAdd. Dodawanie danych odbywa się całkiem prosto:

private void btnAdd_Click(object sender, RoutedEventArgs e)
{
    DataRow oDataRow = m_oDataTable.NewRow();
    oDataRow[0] = 1;
    oDataRow[1] = "Jan";
    oDataRow[2] = "Kowalski";
    oDataRow[3] = 25;
    m_oDataTable.Rows.Add(oDataRow);
    m_oDataAdapter.Update(m_oDataSet);
}

Najpierw, musimy utworzyć pusty obiekt DataRow o strukturze zgodnej z naszą tabelą. Następnie w liniach 4-7 wypełnić jego kolumny. Do kolumn można się odwołać za pomocą indeksów, albo ich nazw. W tym przypadku, mamy cztery kolumny więc kwestia wykorzystania indeksów w tej sytuacji wydaje się być szybka i prosta.

W linii 8, dodajemy wiersz do naszej tabeli, a w linii 9 aktualizujemy źródło danych. Dzięki temu zostanie zaktualizowany nasz plik bazy danych i tym samym automatycznie odświeży się widok listy osób w aplikacji:)

Po dodaniu przykładowego wiersza, aplikacja może wyglądać mniej więcej tak jak na screenie 4:

Edycja danych

Edycja danych będzie odrobinę bardziej interaktywna, ponieważ będziemy zmieniać wiersz zaznaczony na liście. W celu skrajnego uproszczenia, będziemy zmieniać nazwisko i wiek na te z góry ustalone, chodzi po prostu o ukazanie samego mechanizmu.

private void btnEdit_Click(object sender, RoutedEventArgs e)
{
	if (0 == lstItems.SelectedItems.Count)
	{
		return;
	}
	DataRow oDataRow = ((DataRowView)lstItems.SelectedItem).Row;
	oDataRow.BeginEdit();
	oDataRow[2] = "Nowak";
	oDataRow[3] = 28;
	oDataRow.EndEdit();
	m_oDataAdapter.Update(m_oDataSet);
}

Na początku, musimy sprawdzić, czy jakiś wiersz został zaznaczony na liście (3-6), musimy mieć w końcu co edytować - prawda;)?

Jeśli jakiś wiersz został zaznaczony, to pobieramy go z listy za pomocą metody SelectedItem (ewentualnie można pobrać kolekcję zaznaczonych wierszy przy użyciu metody SelectedItems). W tym momencie otrzymujemy obiekt DataRowView, z którego możemy jednak bez problemu wyciągnąć DataRow (7). W linii 8 rozpoczynamy edycję wybranego wiersza.

W tym przypadku skorzystaliśmy z metody BeginEdit, która umożliwia lepsze zarządzanie edycją wiersza. Dzięki użyciu tej techniki, możemy łatwo się wycofać ze zmian np. w sytuacji gdy na dialogu edycji użytkownik kliknie na przycisk Anuluj. Wtedy wystarczy zastosować na wierszu metodę CancelEdit aby wycofać wszelkie wprowadzone przez nas zmiany. Metoda EndEdit (11), zatwierdza wprowadzone zmiany w wierszu. Cały ten mechanizm można przyrównać do transakcji bazy danych, ponieważ mamy adekwatne operacje. Odpowiednio: Begin, Rollback oraz Commit.

Konkretne wartości w danym wierszu zmieniamy w bardzo prosty sposób, po prostu przypisując do istniejących kolumn nowe wartości.

Cały proces kończymy w linii 12, znaną już Wam wcześniej metodą aktualizacji adaptera danych.

Usuwanie danych

Ostatnia operacja związana z przetwarzaniem danych, to oczywiście opcja ich usunięcia. W tym przypadku sprawa jest naprawdę prosta:

private void btnDelete_Click(object sender, RoutedEventArgs e)
{
	if (0 == lstItems.SelectedItems.Count)
	{
		return;
	}
	DataRow oDataRow = ((DataRowView)lstItems.SelectedItem).Row;
	oDataRow.Delete();
        m_oDataAdapter.Update(m_oDataSet);
}

Najpierw sprawdzamy w liniach 3-6, czy element został zaznaczony na liście. Robimy to dokładnie w taki sam sposób jak przy edycji. Następnie w linii 7 pobieramy ten wiersz, co przebiega również identycznie jak przy edycji. W linii 8 aplikujemy metodę Delete naszemu wierszowi, a cały proces kończymy oczywiście aktualizacją adaptera danych (9).

Nie muszę Wam zapewne przypominać, że w rozwiązaniu produkcyjnym warto się zapytać użytkownika, czy na pewno jest pewien swojej decyzji;)

Zamykanie okna

Choć nasz DataAdapter w teorii powinien posprzątać się sam przy zamykaniu aplikacji, ale jednak warto zadbać by zrobił to na pewno. Dlatego też pisząc kod XAML zadeklarowaliśmy metodę, która będzie obsługiwać zdarzenie Closing okna. Takie zdarzenie będzie się aktywować w momencie zamykania aplikacji (warto wiedzieć, że w tym momencie, możemy np. zatrzymać zamykanie programu) i wykona metodę Dispose na adapterze danych:

private void Window_Closing(object sender, 
	System.ComponentModel.CancelEventArgs e)
{
	if (null != m_oDataAdapter)
	{
		m_oDataAdapter.Dispose();
		m_oDataAdapter = null;
	}
}

Podsumowanie

W dzisiejszym wpisie, udało się nam utworzyć bazę danych SQLite, skonfigurować Visual Studio i stworzyć działający projekt. Tak jak podkreślałem w całym wpisie, nie jest to oczywiście rozwiązanie produkcyjne. Brakuje kodu obsłgującego błędy, podziału na klasy oraz okien dialogowych umożliwiających zmianę danych widocznych na liście. Mimo wszystko powyższy kod jednak działa i powinien być dobrą podstawą do stworzenia czegoś większego.

W razie pytań/sugestii zapraszam do komentarzy:)

P.S. Gotowy projekt znajdziecie w dziale pliki.

UWAGA - Problem z Could Not Find Assembly...

W przypadku gdy na komputerze docelowym (nie deweloperskim) pojawi się błąd o niemożności odnalezienia biblioteki SQLite (Could not find assembly...), może być konieczne zainstalowanie tej biblioteki.

Biblioteka SQLite jest skompilowania w trybie Mixed mode assembly i zawiera w sobie kod C++.

Alternatywnie możecie do projektu dołączyć bibliotekę DLL z lokalizacji C:\windows\system32\msvcr100.dll w Waszym systemie. W jednej mojej aplikacji dołączyłem wyżej wymieniony plik do projektu instalatora, dzięki czemu instaluje się on razem z aplikacją. Plik ten powinien trafiać tam gdzie biblioteka SQLite.

Data ostatniej modyfikacji: 21.11.2013, 08:02.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus