Czwarta część kursu dotyczy współpracy ASP.NET z bazami danych. Omówione w niej zostały główne techniki dostępu do danych pochodzących z różnych źródeł, sposoby edycji informacji w bazie oraz mechanizm wiązania danych z kontrolkami webowymi. Autor skupia się nie tylko na zagadnieniach technicznych, lecz także poświęca nieco miejsca zagadnieniom planowania aplikacji bazodanowej. Na potrzeby tego odcinka powstała kompletna aplikacja - ankieter do zastosowania na dowolnej stronie.
Tworząc witryny internetowe prędzej czy później staje się przed potrzebą przechowania gdzieś jakichś danych. Czy są to informacje o odwiedzających, oferta sklepu, newsy na witrynie informacyjnej, czy wypowiedzi na forum dyskusyjnym, zawsze występuje jakaś funkcjonująca na serwerze internetowym składnica danych. W zależności od ich ilości, rodzaju, czy wymaganej funkcjonalności może nią być np. zwykły plik tekstowy, dokument XML albo baza danych.
Każde z wyżej wymienionych rozwiązań ma swoje wady i zalety - przykładowo, użycie XML pozwala na skonstruowanie dynamicznej witryny gdy nie ma możliwości skorzystania z serwera baz danych - jednak największą elastyczność, wydajność i skalowalność uzyskamy przechowując nasze dane w bazie.
Większość środowisk do tworzenia dynamicznych serwisów internetowych - czy to PHP, ASP, czy np. servlety Java - oferuje jakieś mechanizmy dostępu do danych. Najczęściej sprowadzają się one do pakietu funkcji lub obiektów pozwalających na wysyłanie do serwera SQL zapytań oraz odbieranie i przeglądanie jego odpowiedzi. W przypadku ASP.NET odpowiada za to infrastruktura ADO.NET, która oprócz podstawowych operacji na danych udostępnia także wiele usług dodatkowych, zwiększających produktywność programisty lub skalowalność tworzonego rozwiązania.
.NET Framework a bazy danych
ADO.NET umożliwia wygodny, spójny i jednorodny dostęp do różnorodnych źródeł danych. Źródłem danych może być serwer baz danych, taki jak MS SQL Server lub Oracle, plik XML, czy też dowolny magazyn udostępniający dane przez interfejs ODBC lub OLE DB - na przykład magazyn danych serwera Exchange. Mówiąc ogólniej, ADO.NET może korzystać z dowolnego źródła danych, dla którego istnieje tzw. dostawca danych - .NET Framework Data Provider.
DataProvider
Dostawca danych jest zestawem klas pozwalających na korzystanie z określonego źródła danych. Wśród nich znajdują się zawsze cztery następujące, podstawowe klasy:
| Klasa | Opis |
| Connection |
Umożliwia nawiązanie połączenia z określonym źródłem danych. |
| Command |
Wywołuje polecenie na źródle danych. Udostępnia kolekcję parametrów (Parameters) i zawsze działa w kontekście otwartego połączenia (Connection) |
| DataReader |
Udostępnia jednokierunkowy (rekord po rekordzie) strumień danych ze źródła, w trybie 'tylko do odczytu'. Może zawierać więcej niż jeden zestaw rekordów - w takim przypadku korzystamy z nich kolejno (pierwszy zestaw, potem drugi, trzeci...) |
| DataAdapter |
Wypełnia DataSet (patrz niżej) danymi pochodzącymi ze źródła oraz umożliwia aktualizacje danych w źródle na podstawie DataSet-u. |
.NET Framework w wersji 1.1 udostępnia domyślnie dostawców danych dla baz danych MS SQL Server i Oracle oraz źródeł danych OLE DB i ODBC. Zestaw ten jest wystarczający dla większości zastosowań. W tej części artykułu skupię się przede wszystkim na współpracy z bazą MS SQL Server/MSDE, a ponadto przedstawię w jaki sposób można połączyć się ze źródłem danych ODBC, jakim jest popularny serwer MySQL.
DataSet
Drugim głównym elementem ADO.NET jest obiekt DataSet. Jest to uniwersalny magazyn danych, pozwalający na przechowywanie dowolnych informacji - w szczególności wycinka bazy danych - w oderwaniu od źródła. Częstym scenariuszem pracy z DataSet-em jest wypełnienie go za pomocą DataAdapter-a, wykonanie jakichś operacji na danych, po czym zaktualizowanie źródła danych (oryginalnego lub zupełnie innego) za pośrednictwem tego samego lub innnego DataAdapter-a. Przykład w dalszej części artykułu powinien obrazowo wyjaśnić tą koncepcję.
DataSet jest swojego rodzaju lokalną kopią bazy danych lub jej fragmentu. Jest to dość zaawansowany obiekt: może zawierać wiele tablic z danymi, pozwala na definiowanie związków pomiędzy nimi oraz nakładania ograniczeń na dane, a także umożliwia przeszukiwanie i wybieranie określonych rekordów. Ponadto, obok tabelarycznego widoku danych zorganizowanych w postaci wierszy i kolumn, możliwe jest interpretowanie DataSet-u jako dokumentu XML - i vice versa. Tym zagadnieniem zajmiemy się w jednym z kolejnych odcinków kursu, niemniej jednak warto już teraz wiedzieć, że istnieje taka możliwość.
Z powyższego wyłania się już pewien obraz całości architektury ADO.NET: Po jednej stronie dostawca danych, ściśle związany z określonym źródłem danych, a po drugiej współpracujący z nim, zaawansowany obiekt DataSet, za pomocą którego można w sposób jednolity traktować informacje różnego typu i pochodzenia. Poniższy rysunek przedstawia wzajemne relacje pomiędzy tymi elementami:

Trudne decyzje
Przyglądając się liście podstawowych obiektów wchodzących w skład dostawcy danych .NET Framework, można zauważyć że znajduje się na niej jeden obiekt pozwalający połączyć się ze źródłem danych, jeden umożliwiający wydawanie mu poleceń, oraz dwa służące do odczytywania zeń danych: DataReader i DataSet. Dlaczego dwa? Wynika to z różnych scenariuszy dostępu do danych. DataReader to bardzo prosty, lekki i wygodny w użyciu obiekt, pozwalający na szybkie odczytanie informacji w trybie rekord po rekordzie i zrobienie z nimi "czegoś" - na przykład wyświetlenie na stronie w postaci listy. DataReader po przejściu do kolejnego rekordu "zapomina" wszystkie informacje o poprzednim. To podejście jest bardzo zbliżone do stosowanego w środowiskach takich, jak ASP lub PHP.
DataAdapter, z drugiej strony, sam w sobie nie pozwala łatwo wykorzystać pobieranych ze źródła danych. Funkcjonalność zyskuje dopiero w zestawieniu z obiektem DataSet, adaptując dane pochodzące ze źródła do ogólnej struktury narzucanej przez DataSet - i z powrotem. Po wypełnieniu DataSet-u możliwe są dowolne, opisane wyżej operacje na tych danych, w całkowitym oderwaniu od źródła, z którego pochodzą. Oczywiście ma to swoją cenę: większe wymagania pamięciowe, zmniejszona wydajność i bardziej skomplikowany kod.
Z tego powodu jedną z najważniejszych umiejętności związanych z programowaniem ADO.NET jest zdolność do podjęcia właściwej decyzji co do metody dostępu do danych. Kiedy używać DataReader-a, a kiedy tandemu DataAdapter + DataSet? Oraz, w tym drugim przypadku, jakie dane załadować do DataSet-u? Przecież nie można (a raczej nie opłaca się) ładować do niego całej zawartości bazy danych. W dalszej części tego artykułu postaram się pokazać, w jakich sytuacjach warto korzystać z pierwszego, a w jakich z drugiego podejścia. Na początku jednak należy uzyskać połączenie z bazą danych i nauczyć się wydawać jej polecenia.
MSDE i MS SQL Server
MS SQL Server jest okrętem flagowym wśród baz danych firmy Microsoft. Produkt ten cechuje się bardzo bogatą funkcjonalnością (samo jej wymienienie to temat na osobny artykuł), wysoką wydajnością i skalowalnością - od bardzo małych systemów obsługujących kilku klientów po ogromne hurtownie danych, operujące na terabajtach informacji. MSDE jest w pełni zgodną z SQL Serverem bazą danych, niemal nie ustępującą mu pod względem funkcjonalności, ograniczoną jedynie pod względem wydajnościowym i nie nadającą się do użycia pod dużym obciążeniem. Zaletą MSDE jest to, że jest to produkt darmowy, pozwalający na tworzenie aplikacji bez konieczności inwestowania w zaawansowany sprzęt i drogie oprogramowanie. Dodatkowym ograniczeniem MSDE jest okrojenie go ze wszelkich graficznych narzędzi administracyjnych dostępnych wraz z "dużym" SQL Serverem - nie jest to jednak wielkim problemem, ponieważ większość czynności administracyjnych związanych z tworzeniem baz danych można wykonać bezpośrednio z poziomu Visual Studio .NET. Dalsza część tego artykułu będzie zakładała wykorzystanie bazy MSDE, omawiane zagadnienia dotyczą jednak w takim samym stopniu pełnego MS SQL Servera. Na końcu artykułu można znaleźć link, spod którego można pobrać pakiet instalacyjny MSDE.
Instalacja MSDE
Po pobraniu i rozpakowaniu pakietu instalacyjnego MSDE, należy go zainstalować. Niestety, nie wystarczy do tego celu samo uruchomienie pliku setup.exe - trzeba podać mu jeszcze kilka parametrów:
| Parametr | Opis |
| SAPWD="HasłoAdministratora" |
Określa hasło, którego będzie używało konto administracyjne sa. |
| INSTANCENAME="NazwaInstalacji" |
Określa nazwę instalacji. Możliwe jest uruchomienie na jednym komputerze kilku niezależnych instalacji MSDE, pod warunkiem że będą się różniły nazwami. Jeżeli parametr ten zostanie pominięty, to serwer zostanie uruchomiony jako instalacja domyślna. |
DISABLENETWORKPROTOCOLS=n (n=0..1) |
Określa, czy dana instalacja serwera będzie przyjmowała połączenia od aplikacji działających na innych komputerach. Domyślnie, lub po podaniu n=1, połączenia sieciowe nie będą dostępne. Jeżeli serwer ma działać na tym samym komputerze, co witryna internetowa, to najlepiej pozostawić ustawienie domyślne. |
| SECURITYMODE=SQL |
Określa, czy uwierzytelnianie do serwera będzie działało w trybie mieszanym (konta Windows + konta serwera SQL), czy tylko Windows (gdy parametr pominięty). Warto włączyć uwierzytelnianie mieszane - daje to większą elastyczność w określaniu dostępu do serwera i baz danych. |
| DATADIR="ŚcieżkaPlikówDanych" |
Określa lokalizację systemowych plików z danymi oraz domyślną lokalizację dla baz użytkownika. Ścieżka musi być zakończona znakiem \. |
| TARGETDIR="ŚcieżkaInstalacji" |
Określa folder instalacyjny serwera. Ścieżka musi być zakończona znakiem \. |
Przykładowe, zalecane na potrzeby niniejszego kursu polecenie instalacji (w przypadku instalacji na tym samym komputerze, co serwer internetowy):
setup.exe SAPWD="mojehaslo" SECURITYMODE=SQL
Założenie bazy danych
Po zainstalowaniu serwera MSDE należy założyć na nim pustą bazę danych. Ponieważ MSDE nie zawiera żadnych graficznych narzędzi do zarządzania serwerem, to musimy posłużyć się zamiast tego pakietem Visual Studio .NET. Środowisko to udostępnia potężne narzędzie do zarządzania usługami serwerowymi: panel Server Explorer. Domyślnie jest on dostępny z lewej strony ekranu, w tym samym miejscu co paleta kontrolek (trzeba tylko wybrać odpowiednią zakładkę):

Aby utworzyć nową bazę danych, trzeba w panelu Server Explorer wybrać odpowiedni komputer, rozwinąć węzeł SQL Servers i kliknąć prawym klawiszem myszy odpowiednią instalację serwera (domyślna ma taką samą nazwę, jak komputer na którym działa). Następnie należy wybrać opcję New Database i wpisać nazwę tworzonej bazy w polu New Database Name (np. KursAspNet). Opcję Use Windows Integrated Securitynależy pozostawić zaznaczoną, co zagwarantuje, że użytkownik z którego korzystamy będzie właścicielem nowo tworzonej bazy - czyli będzie miał w jej kontekście pełne uprawnienia.
Połączenie robocze Visual Studio
Pracując z bazami danych w Visual Studio .NET spotykamy się z dwoma rodzajami połączeń z bazą danych, co dla wielu początkujących użytkowników jest dość mylące (dla mnie kiedyś było). Jedno z nich, to bardziej oczywiste, to połączenie wykorzystywane przez tworzoną aplikację podczas jej działania (run-time). Uruchomiony program w jakiś sposób musi komunikować się z bazą danych i w tym celu tworzy sobie połączenie (a konkretnie obiekt typu Connection). Drugim rodzajem połączenia jest połączenie robocze, wykorzystywane podczas tworzenia aplikacji w Visual Studio (design-time). W kontekście tego połączenia uruchamiane są wszystkie kreatory wspomagające proces projektowania, a ponadto jest ono bardzo często wykorzystywane jako szablon dla obiektów połączenia, które potem będą używane w trybie run-time. Choć nie jest to konieczne, to tworzenie aplikacji warto rozpocząć od kontrolowanego zdefiniowania połączenia design-time - w przeciwnym razie zostanie ono wygenerowane podczas korzystania z któregoś z kreatorów, w chwili gdy będziemy zajęci innymi zagadnieniami.
Aby utworzyć nowe połączenie robocze, należy kliknąć prawym przyciskiem myszy węzeł Data Connections w panelu Server Explorer i wybrać polecenie Add Connection. Zostanie wyświetlone okno dialogowe, w którym należy podać parametry niezbędne do nawiązania komunikacji z bazą danych: nazwę serwera, typ uwierzytelniania (oraz nazwę i hasło użytkownika w przypadku uwierzytelniania SQL) oraz nazwę bazy danych na serwerze:

Na koniec można kliknąć przycisk Testuj Połączenie i, jeżeli wszystko jest w porządku, OK. W panelu Server Explorer powinno pojawić się nowe połączenie o nazwie postaci nazwa_serwera.nazwa_bazy.nazwa_użytkownika - u mnie goofy.KursAspNet.dbo. Co to za użytkownik dbo? W rzeczywistości nie jest to żaden fizyczny użytkownik serwera SQL, a jedynie alias określający właściciela bazy danych. W naszym przypadku oznacza to, że wszystko jest tak, jak założyliśmy: połączyliśmy się z bazą danych używając tego samego konta, którego używa właściciel naszej nowej bazy danych. W nagrodę można rozwinąć węzeł połączenia i przyjrzeć się elementom znajdującym się poniżej. Najważniejszymi z nich (tabelami - Tables - i procedurami składowanymi - Stored Procedures) zajmiemy się w dalszej części tego odcinka kursu, w rozdziale Podstawy SQL. Trochę później, w kolejnym rozdziale (Prosty odczyt - SqlDataReader) pokażę w jaki sposób można tworzyć połączenia z bazą danych w trybie run-time. Teraz zajmiemy się planowaniem naszej pierwszej aplikacji bazodanowej.
Planowanie aplikacji
Tym sposobem doszliśmy do najciekawszego chyba elementu dzisiejszego odcinka - aplikacji korzystającej z bazy danych. Będzie to ankieta, moduł znany z wielu różnych portali i innych witryn internetowych. Jest to prosty przykład pozwalający na zademonstrowanie różnych technik dostępu do danych. Dzięki naszej ankietce jeszcze dzisiaj będziemy mieli okazję spotkać się z obiektem DataReader i bliżej zaznajomić się z obiektami DataAdapter i DataSet. Zacznijmy jednak od określenia funkcjonalności aplikacji.
Moduł kliencki
- Moduł ankiety powinien wyświetlać jej nazwę (opis) oraz listę dostępnych opcji.
- Moduł powinien umożliwiać wybranie jednej i tylko jednej spośród dostępnych opcji oraz oddanie na nią głosu.
- Moduł powinien zabezpieczać przed ponownym oddawaniem głosu przez tą samą osobę.
- Moduł ankiety powinien wyświetlać jedną - aktualną - ankietę. Jeżeli nie ma aktualnej ankiety, to moduł powinien być niewidoczny.
- Aktualna ankieta to taka, której data rozpoczęcia już minęła, a data zakończenia jeszcze nie nastąpiła.
- Chcemy mieć możliwość oglądania ankiet (aktualnej i poprzednich) oraz ich wyników w postaci wykresów słupkowych.
- Ankiety zdefiniowane przez administratora jako ukryte nigdy nie będą "aktualnymi" ani nie pojawią się na liście poprzednich ankiet
- Moduł ankiety powinien być wykonany jako kontrolka, co pozwoli na umieszczenie go w dowolnym miejscu strony bez dodatkowego programowania.
Strona administracyjna
- Lista ankiet, z możliwością przejścia do edycji lub usunięcia ankiety, oraz jej ukrywania/odkrywania.
- Możliwość tworzenia nowej ankiety.
- Dla nowej ankiety: określanie opisu, tego czy jest ona ukryta, daty początkowej i końcowej oraz listy opcji.
- Zapisywanie gotowej ankiety do bazy danych.
Dane w bazie są przechowywane w tabelach. Po krótkiej analizie powyższych założeń można ustalić, jakie tabele będą wykorzystywane przez aplikację i jakie dane będą w nich przechowywane:
- Tabela Ankiety
- Opis ankiety
- Początkowa data wyświetlania
- Końcowa data wyświetlania
- Informacja o ukryciu ankiety
- Tabela Opcje
- Treść opcji
- Liczba głosów na tą opcję
Gdy już wiemy jak aplikacja ma działać i na jakich danych będzie pracować, można przejść do konkretów: utworzenia struktury bazy danych i kodu pobierającego dane.
Podstawy SQL
Tworzenie tabel
Korzystając z panelu Server Explorer i narzędzi udostępnianych przez Visual Studio .NET można łatwo tworzyć i modyfikować obiekty bazy danych. Aby dodać nową tabelę trzeba rozwinąć węzeł połączenia, kliknąć prawym przyciskiem myszy na węźle Tables i wybrać polecenie New Table. Za pomocą wyświetlonego edytora można utworzyć strukturę tabeli (dokładne omówienie typów kolumn znajduje się na końcu artykułu, w Dodatku.

| Kolumna | Typ | Uwagi |
| Tabela "Ankiety" |
| ank_id |
uniqueidentifier |
Aby wygodnie pracować z danymi pochodzącymi z bazy, trzeba w jakiś sposób identyfikować kolejne rekordy (wiersze) w danej tabeli. Z tego powodu warto w każdej z tabel utworzyć specjalne pole identyfikacyjne, tzw. klucz główny. Wartości klucza nigdy się nie powtarzają, więc za ich pomocą można jednoznacznie określić konkretny zapis w bazie. Aby oznaczyć dane pole jako klucz główny, należy kliknąć je prawym przyciskiem myszy i wybrać polecenie Set Primiary Key. |
| ank_opis |
varchar(200) |
Definiując pole jako znakowe trzeba określić jego maksymalną długość - w tym przypadku 200 znaków. |
| ank_start |
datetime |
W MS SQL Serverze nie ma oddzielnych typów pól dla daty i czasu, korzystamy więc z tego co jest: 8-bajtowego pola typu DateTime. |
| ank_koniec |
datetime |
|
| ank_ukryta |
bit |
Pole typu bit pozwala na zapisanie wartości typu 0/1 (nie/tak). |
| Tabela "Opcje" |
| opc_id |
uniqueidentifier |
Klucz główny! |
| opc_ankieta |
uniqueidentifier |
Trzeba w jakiś sposób powiązać opcję z ankietą, do której należy. Do tego przyda się pole typu uniqueidentifier, w którym będzie można zapisać identyfikator nadrzędnej ankiety. |
| opc_opis |
varchar(200) |
|
| opc_glosy |
int |
|
Na co warto zwrócić uwagę:
- Pola-identyfikatory w obydwu tabelach mają ustawioną wartość domyślną (Default Value) na
newid(). Jest to funkcja zwracająca nowy unikalny identyfikator. Zastosowanie jej jako wartości domyślnej pozwala zagwarantować, że nowo dodawane rekordy będą miały prawidłowe i nie powtarzalne wartości klucza głównego.
- Osoby, które miały już do czynienia z bazami danych, zapewne zastanawiają się dlaczego zalecam użycie typu
uniqueidentifier dla identyfikatorów rekordów. Bardziej typowym rozwiązaniem jest skorzystanie z typu int i włączenie dla niego autoinkrementacji - czyli automatycznego zwiększania o 1 wartości pola dla każdego kolejnego rekordu. Rozwiązanie to wydaje się prostsze i oszczędniejsze (int - 4 bajty, uniqueidentifier - 16 bajtów), ale ma jedną wadę: gdy korzysta się z datasetu, to rekordy są tworzone w odłączeniu od bazy danych. Podczas synchronizowania datasetu z bazą danych, z której korzysta kilku użytkowników jednocześnie, mogą pojawić się konflikty wartości inkrementowanych automatycznie. Gdy dodaje się rekord do tabeli datasetu, nigdy nie wiadomo czy w międzyczasie w bazie danych nie powstał już rekord o tej samej wartości klucza głównego typu int. Unikalny identyfikator - uniqueidentifier - skutecznie nas przed taką sytuacją zabezpiecza.
- Osoby mające doświadczenie z Accessem lub MS SQL Serverem znają pojęcie związków integralności, czasem błędnie nazywanych relacjami. W każdej z tych baz danych istnieje możliwość powiązania ze sobą dwóch tabel w celu wymuszenia poprawności zależności pomiędzy znajdującymi się w nich rekordami. W naszym przypadku można by taki związek utworzyć pomiędzy kluczem głównym (
ank_id) tabeli Ankiety oraz polem opc_ankieta w tabeli Opcje. Nie będziemy jednak tego robić, a dlaczego - wyjaśnię omawiając dalej obiekt DataSet.
- Pole ank_ukryta ma domyślnie wartość 1, co będziemy interpretować, jako domyślne ukrycie ankiety. Nie chcemy publikować ankiet dopóki nie będziemy absolutnie pewni, że nie ma w nich np. błędów ortograficznych.
- Pole opc_glosy ma domyślnie wartość 0 (brak głosów).
- Ponieważ nie przewidujemy traktowania żadnego z pól jako opcjonalnego, to można we wszystkich wyłączyć opcję w kolumnie Allow Null. Wymusi to konieczność wypełniania wszystkich kolumn w tabeli (oprócz posiadających wartość domyślną).
Przed przejściem dalej proponuję ręcznie wypełnić tabele przykładowymi danymi. W tym celu należy kliknąć nazwę tabeli prawym przyciskiem myszy i wybrać polecenie Retrieve Data From Table. Następnie można wpisywać kolejne rekordy korzystając z wyświetlonej tabeli, pomijając kolumny identyfikatorów tabel (funkcja newid() nada identyfikatory automatycznie). Nie należy się przejmować, że wpisywane rekordy znikają po przejściu do nowego wiersza - one trafiają do bazy, po prostu nie są wyświetlane. Pojawią się po odświeżeniu tabeli (przycisk z czerwonym wykrzyknikiem). Wypełniając tabelę Opcje, w polu opc_ankieta wklejamy wartości skopiowane z pola ank_idw tabeli Ankiety (nadane przez system identyfikatory będą widoczne po odświeżeniu tabeli).
Pobieranie danych z bazy
Aby pobrać dane z bazy, należy zadać jej pytanie sformułowane w języku SQL. Najprostsza postać takiego pytania może mieć formę:
Proste tłumaczenie na język polski: WYBIERZ * Z_TABELI Ankiety. Znak gwiazdki (*) oznacza "Wszystkie pola" i zamiast niego, można by napisać:
SELECT ank_id, ank_opis, ank_start, ank_koniec, ank_ukryta FROM Ankiety
Powyższe zapytanie zwraca po prostu wszystkie rekordy z bazy. Aby uzależnić od czegoś to, jakie rekordy zostaną wybrane, należy posłużyć się klauzulą WHERE, natomiast uporządkowanie rekordów można wymusić za pomocą klauzli ORDER BY:
SELECT ank_id, ank_opis, ank_start, ank_koniec, ank_ukryta FROM Ankiety
WHERE ank_start
Powyższy kod zwróci ankiety, które już się rozpoczęły i nie są ukryte, uporządkowane malejąco wg daty rozpoczęcia (od najnowszej do najstarszej). Porządek rosnący uzyskujemy pomijając słowo DESC.
Wszystkie relacyjne bazy danych obsługują polecenia SQL podobne do powyższych. MS SQL Server i MSDE udostępniają dodatkowo bardzo potężny i prosty w użyciu mechanizm tzw. procedur składowanych (ang. Stored Procedures). W skrócie, jest to zapytanie SQL (lub kilka zapytań) zapisanych w serwerze baz danych w postaci nazwanego pakietu. Procedura składowana może posiadać parametry, zawierać dowolne elementy języka TSQL (Transact SQL jest rozszerzonym językiem SQL obsługiwanym przez MS SQL Server i MSDE) i zwracać wiele zbiorów rekordów. Jako przykład, spróbujemy przygotować procedury składowane realizujące kolejne punkty z listy wymagań dla modułu klienckiego naszej aplikacji. Aby utworzyć nową procedurę, należy kliknąć prawym przyciskiem myszy węzeł Stored Procedures w panelu Server Explorer i wybrać polecenie New Stored Procedure.
Wszystkie opisane niżej procedury definiujemy w bazie danych, na razie w oderwaniu od kodu aplikacji. W dalszej części artykułu pokażę jak można z nich korzystać i jak pracować ze zwracanymi przez nie wynikami (zestawami rekordów).
Wyświetlanie nazwy ankiety i listy opcji
Procedura zwróci dwa zbiory rekordów: pierwszy zawierający jeden rekord opisujący wybraną ankietę, drugi zawierający dostępne dla niej opcje. Parametrem procedury jest identyfikator ankiety. Jeżeli zostanie on pominięty, to serwer spróbuje wyszukać pierwszą z nie-ukrytych ankiet, które aktualnie obowiązują.
-- Nagłówek procedury i deklaracja parametru, posiadającego wartość domyślną NULL
CREATE PROCEDURE AnkietyJedna @ank_id uniqueidentifier = NULL
AS
-- Gdy nie podano parametru, wyszukanie pierwszej pasującej ankiety
IF @ank_id IS NULL
SET @ank_id = (
SELECT TOP 1 ank_id FROM Ankiety
WHERE ank_start -- Deklaracja i ustalenie wartości zmiennych zawierających sumę oraz maksymalną liczbę głosów
-- danej ankiety (potrzebne do obliczenia udziałów procentowych)
DECLARE @suma_glosow int
SET @suma_glosow = (SELECT SUM(opc_glosy) FROM Opcje WHERE opc_ankieta = @ank_id)
DECLARE @max_glosow int
SET @max_glosow = (SELECT MAX(opc_glosy) FROM Opcje WHERE opc_ankieta = @ank_id)
-- Pierwszy zbiór rekordów: wybrana ankieta
SELECT * FROM Ankiety WHERE ank_id = @ank_id
-- Drugi zbiór rekordów: opcje dla wybranej ankiety -- Widać przykład zastosowania konstrukcji CASE, zwracającej tutaj odpowiednią wartość procentową -- lub 0, w zależności od wartości określonych powyżej zmiennych. -- Zademonstrowano też działanie operatora AS, pozwalającego nadawać 'w locie' nazwy dla kolumn -- wyliczanych (tutaj: opc_procent oraz opc_procent_max).
SELECT *,
CASE @suma_glosow WHEN 0 THEN 0 ELSE opc_glosy*100/@suma_glosow END AS opc_procent,
CASE @max_glosow WHEN 0 THEN 0 ELSE opc_glosy*100/@max_glosow END AS opc_procent_max
FROM Opcje WHERE opc_ankieta = @ank_id
Głosowanie
Do głosowania nie jest potrzebe pobieranie danych z tabeli, a jedynie ich modyfikacja. Służy do tego polecenie UPDATE języka SQL:
CREATE PROCEDURE AnkietyGlosuj @opc_id uniqueidentifier
AS
UPDATE Opcje SET opc_glosy = opc_glosy+1 WHERE opc_id = @opc_id
-- Tłumaczenie z SQL na język polski: ZAKTUALIZUJ tabelę Opcje ZMIENIAJĄC -- wartość pola opc_glosy na wartość wyższą o 1 DLA REKORDÓW o zadanym identyfikatorze
Wyświetlanie listy ankiet
Tutaj zasada jest prosta: Administrator będzie widział wszystkie ankiety, pozostali użytkownicy jedynie te, które się rozpoczęły przed dniem dzisiejszym i nie są ukryte.
CREATE PROCEDURE AnkietyLista @admin bit = 0
AS
SELECT * FROM Ankiety
WHERE (ank_start
... i to wszystko, co nam będzie potrzebne do sporządzenia modułu klienckiego. Pora wrócić do programowania w ASP.NET. Ponieważ moduł ten będzie jedynie wyświetlał dane pochodzące z bazy, to nie jest nam potrzebna rozbudowana funkcjonalność DataSet-u. Prosty i szybki w działaniu DataReader będzie idealnym rozwiązaniem dla modułu, który najprawdopodobniej stanie się stałym elementem każdej ze stron witryny internetowej, na której będzie umieszczony.
Proste operacje - SqlDataReader i SqlCommand
Zanim zaczniemy tworzyć funkcjonalność modułu klienckiego, musimy w znany już sposób utworzyć nową aplikację webową. Na początku będzie się ona składała z jednej strony test.aspx na której umieścimy naszą kontrolkę - ankietę, oraz jednej kontrolki użytkownika (ankieta_box.ascx). Zaczniemy od przeciągnięcia pustej jeszcze kontrolki na stronę testową i zapisania projektu, po czym przejdziemy do projektowania interfejsu użytkownika.
Interfejs kontrolki ankieta_box

| Paleta/kontrolka | Atrybut | Wartość |
| HTML / Table |
width |
100% 1 kolumna, 4 wiersze (funkcje edycji tabeli są dostępne po kliknięciu jej prawym przyciskiem myszy) W pierwszej komórce napis "Ankieta". |
| Web Forms / Label |
ID |
lblOpis |
| Web Forms / RadioButtonList |
ID |
opc_id |
| Web Forms / Label |
Text ID |
Dziękujemy za udział w tej ankiecie lblJuzGlosowal |
| Web Forms / RequiredFieldValidator |
ErrorMessage ControlToValidate |
Wybierz opcję opc_id |
| Web Forms / Button |
Text Width ID |
Głosuj 70px btnGlosuj |
| Web Forms / Button |
Text Width ID |
Wyniki 70px btnWyniki |
Warto zwrócić uwagę na kontrolkę RadioButtonList. Wygląda ona niepozornie, ale po podczepieniu do źródła danych potrafi wygenerować dowolnej długości listę opcji opisanych poszczególnymi rekordami tego źródła. Tak, bez żadnej linijki kodu, bez pętli itp. Kto się w tej chwili zaczął uśmiechać do monitora?
W tym momencie można by zacząć oprogramowywać poszczególne zdarzenia naszej kontrolki, umieszczając kod dostępu do danych bezpośrednio w procedurach obsługi tych zdarzeń. Chociaż w takim podejściu nie ma nic bardzo niewłaściwego, to lepszym rozwiązaniem jest jednak utworzenie oddzielnego komponentu dostępu do danych i umieszczenie w nim wszystkich procedur niezbędnych do komunikacji z serwerem.
Komponent dostępu do danych
Aby dodać do projektu nowy komponent, należy kliknąć nazwę projektu prawym przyciskiem myszy i wybrać polecenie Add/Add Component. Nadajmy mu nazwę Ankieta_db.cs.
Visual Studio .NET w bardzo wygodny sposób wspomaga tworzenie obiektów dostępu do danych. Po utworzeniu komponentu, naszym oczom ukazuje się pusty obszar projektowy. Aby wygenerować obiekty typu SqlCommand, pozwalające na wywoływanie naszych procedur składowanych, wystarczy poprzeciągać te procedury z panelu Server Explorer do obszaru projektowego. Po przeciągnięciu trzech procedur składowanych powinny powstać trzy obiekty SqlCommand oraz jeden obiektu typu SqlConnection, którego atrybuty zostały już ustalone na podstawie utworzonego uprzednio połączenia roboczego. Warto zwrócić także uwagę na atrybut Connection nowych obiektów sqlCommand - wskazuje on na nowo wygenerowany obiekt sqlConnection1 - oraz na kolekcję Parameters, w której automatycznie pojawiły się elementy typu SqlParameter odpowiadające parametrom procedur składowanych. Nadajmy powstałym obiektom bardziej znaczące nazwy - np. cnn dla połączenia oraz odpowiednio cmdAnkietyJedna, cmdAnkietyLista i cmdAnkietyGlosujdla poszczególnych komend.

Następnie przejdźmy do edytora kodu, w którym przygotujemy funkcje elegancko opakowujące utworzone przed chwilą obiekty (klikamy prawym przyciskiem myszy na polu projektowym i wybieramy polecenie View Code. Dla komend cmdAnkietyJedna i cmdAnkietyLista napiszemy funkcje zwracające dane w postaci obiektu typu SqlDataReader:
public SqlDataReader AnkietyJedna(Guid ank_id)
{
if(ank_id.CompareTo(Guid.Empty)!=0)
cmdAnkietyJedna.Parameters["@ank_id"].Value = ank_id;
cnn.Open();
return cmdAnkietyJedna.ExecuteReader(CommandBehavior.CloseConnection);
}
public SqlDataReader AnkietyLista(bool admin)
{
cmdAnkietyLista.Parameters["@admin"].Value = admin;
cnn.Open();
return cmdAnkietyLista.ExecuteReader(CommandBehavior.CloseConnection);
}
Na co warto zwrócić uwagę:
- Parametry procedur składowanych o typie
uniqueidentifier mapują się na typ Guid biblioteki klas .NET Framework.
- Procedura składowana AnkietyJedna posiadała parametr z podaną wartością domyślną, dlatego wartość parametru
@ank_id obiektu cmdAnkietyJedna ustalamy jedynie wówczas, gdy przekazano do funkcji niepusty Guid.
- Każda z powyższych funkcji zwraca wynik wywołania metody
ExecuteReader odpowiedniego obiektu SqlCommand. Przed wywołaniem tej metody należy pamiętać o otworzeniu połączenia (cnn.Open()) i o jego zamknięciu tuż po wykorzystaniu DataReader-a. Parametr CommandBehavior.CloseConnection metody ExecuteReader automatyzuje tą czynność, powodując zamknięcie połączenia tuż po zamknięciu datareadera.
W przypadku komendy cmdAnkietyGlosuj nie musimy zwracać żadnych danych - chodzi nam jedynie o wywołanie odpowiedniej procedury składowanej na serwerze SQL. Z tego powodu procedura AnkietyGlosujbędzie wyglądała nieco inaczej:
public void AnkietyGlosuj(Guid opc_id)
{
cmdAnkietyGlosuj.Parameters["@opc_id"].Value = opc_id;
cnn.Open();
cmdAnkietyGlosuj.ExecuteNonQuery();
cnn.Close();
}
Jak widać, gdy nie interesują nas dane zwracane przez procedurę składowaną, to możemy skorzystać z metody ExecuteNonQuery(), która nie generuje zbędnego ruchu pomiędzy aplikacją a serwerem SQL.
Ponieważ mamy już gotowy interfejs modułu oraz komponent dostępu do danych, pozostaje jedynie połączyć ze sobą te dwa elementy. Wróćmy do pliku ankieta_box.ascx, tym razem korzystając z edytora kodu.
Bindowanie danych
W poprzedniej części kursu wprowadziłem pojęcie bindowania danych (data binding). Mechanizm ten pozwala na powiązanie dowolnych atrybutów kontrolek do dowolnych danych. Dzięki bindowaniu, bardzo szybko można połączyć interfejs aplikacji z danymi pochodzącymi z bazy danych:
private Guid IdAnkiety = Guid.Empty;
public bool MozeGlosowac = false;
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
Binduj();
else
{
IdAnkiety = new Guid(ViewState["IdAnkiety"].ToString());
MozeGlosowac = SprawdzGlosowanie();
this.DataBind();
}
}
private void Binduj()
{
Ankieta_db db = new Ankieta_db();
SqlDataReader reader = db.AnkietyJedna(IdAnkiety);
if(reader.HasRows)
{
reader.Read();
IdAnkiety = (Guid)reader["ank_id"];
ViewState["IdAnkiety"] = IdAnkiety.ToString();
lblOpis.Text = reader["ank_opis"].ToString();
reader.NextResult();
opc_id.DataSource = reader;
opc_id.DataTextField = "opc_opis";
opc_id.DataValueField = "opc_id";
MozeGlosowac = SprawdzGlosowanie();
this.DataBind();
reader.Close();
}
else
{
reader.Close();
this.Visible = false;
}
}
private bool SprawdzGlosowanie()
{
// Implementacja tymczasowa - póki co nie chcemy blokować głosowania po pierwszym kliknięciu
return true;
}
Na co warto zwrócić uwagę:
- Podczas ładowania strony, jeżeli nie jest ono wywołane akcją użytkownika, czyli np. głosowaniem (
!IsPostBack, już to znamy), wywoływana jest funkcja Binduj().
- Wewnątrz funkcji
Binduj() tworzony jest obiekt klasy Ankieta_db (nasz komponent dostępu do danych), a następnie jest wywoływana jego metoda AnkietyJedna(), domyślnie z parametrem równym Guid.Empty (powinno to spowodować wyszukanie pierwszej aktualnej ankiety).
- Wynik tego wywołania jest umieszczany w zmiennej
reader typu SqlDataReader. Jeżeli procedura zwróciła jakąś ankietę, to odczytywany jest pierszy rekord (opisujący ankietę) i ustawiany jest tekst etykiety lblOpis. Dodatkowo pobierana jest w tym momencie informacja o identyfikatorze ankiety.
- Identyfikator ankiety rejestrowany jest w kolekcji ViewState, którą poznaliśmy w poprzednim odcinku kursu. Dzięki temu będziemy go znać również wtedy, gdy strona zostanie przeładowana w wyniku jakiejś akcji użytkownika (tzn gdy
IsPostBack będzie miało wartość true). W tym momencie nie będziemy już odwoływać się do bazy danych (zysk wydajnościowy!), a zawartość kontrolek będzie utrzymywana automatycznie przez wbudowany w nie mechanizm ViewState.
- Metoda
reader.NextResult() przenosi nas do drugiego zestawu rekordów zwróconych przez procedurę składowaną AnkietyJedna. W trzech kolejnych liniach następuje związanie (bindowanie) listy opc_id (RadioButtonList) z tym zestawem rekordów, zawierającym jak pamiętamy opcje dostępne dla wybranej ankiety. Atrybuty DataTextField i DataValueField określają odpowiednio pola odpowiedzalne za etykietę (opis) danej opcji oraz jej identyfikator.
- Ostateczne związanie danych z kontrolkami następuje po wywołaniu metody
this.DataBind(). Możliwe jest także wiązanie poszczególnych kontrolek, przez wywołanie typu kontrolka.DataBind(). Należy pamiętać o zamknięciu datareadera zaraz po wykorzystaniu.
- Jeżeli nie udało się znaleźć żadnej ankiety spełniającej kryteria, to cała kontrolka
ankieta_box jest ukrywana i zamykany jest obiekt reader.
- Tu i ówdzie pojawiła się nam tajemnicza zmienna
MozeGlosowac, tymaczasowo ustawiana na true za pośrednictwem funkcji SprawdzGlosowanie. Przyda się nam ona do ukrywania elementów interfejsu niepotrzebnych osobie, która już oddała swój głos. Najprościej z niej skorzystać używając ponownie mechanizmu databinding: za pomocą kolekcji DataBindings w oknie Properties wiążemy atrybuty Visible obiektów opc_id i btnGlosuj ze zmienną MozeGlosowac, a w przypadku obiektu lblJuzGlosowal - z negacją tej zmiennej, tj. !MozeGlosowac. Tym sposobem widoczność tych trzech obiektów będzie uzależniona od wartości zmiennej MozeGlosowac.
Głosowanie
W zakresie implementacji kontrolki ankieta_boxpozostało nam jeszcze obsłużyć przyciski Głosuj i Wyniki:
private void btnGlosuj_Click(object sender, System.EventArgs e)
{
Ankieta_db db = new Ankieta_db();
db.AnkietyGlosuj(new Guid(opc_id.SelectedValue));
UstawGlosowanie();
}
private void btnWyniki_Click(object sender, System.EventArgs e)
{
Response.Redirect("Ankieta_wyniki.aspx?ankieta=" + IdAnkiety.ToString());
}
private void UstawGlosowanie()
{
Response.Cookies.Add(new HttpCookie("Ankieta", IdAnkiety.ToString()));
Response.Cookies["Ankieta"].Expires = DateTime.Now.AddMonths(3);
MozeGlosowac = false;
DataBind();
}
Na co warto zwrócić uwagę:
- Obsługa przycisku Głosuj sprowadza się do wywołania metody AnkietyGlosuj naszego komponentu dostępu do danych.
- Obsługa przycisku Wyniki to proste przekierowanie do strony
Ankieta_wyniki.aspx z parametrem - identyfikatorem bieżącej ankiety.
- Pojawiła się dodatkowa funkcja UstawGlosowanie, umieszczająca na komputerze klienta 'cookie' po oddaniu głosu w ankiecie. Cookie może zawierać dowolną informację, trzeba jednak pamiętać o określeniu dlań daty wygaśnięcia - w przeciwnym wypadku będzie ono obowiązywało tylko do momentu zamknięcia przeglądarki internetowej.
- W tym momencie moduł głosowania powinien już działać, pod warunkiem że do tabel wpisane zostały ręcznie jakieś w miarę sensowne dane. Po oddaniu kilku głosów można uzupełnić o prawdziwy kod funkcję
SprawdzGlosowanie().
private bool SprawdzGlosowanie()
{
if(Request.Cookies["Ankieta"] != null)
return Request.Cookies["Ankieta"].Value.ToString() != IdAnkiety.ToString();
else
return true;
}
Wyniki
Strona prezentująca wyniki ankiet jest bardzo prosta w konstrukcji. Poza znanymi już elementami wprowadza bardzo przydatną kontrolkę o nazwie Repeater. Kontrolka ta, w połączeniu ze źródłem danych, pozwala powtarzać pewien fragment kodu HTML dla każdego z rekordów udostępnionych przez źródło. W działaniu jest nieco podobna do poznanej już wcześniej kontrolki RadioButtonList, ale jak się okaże, jest znacznie bardziej elastyczna i daje dużo więcej możliwości.

| Paleta/kontrolka | Atrybut | Wartość |
| HTML / Table |
align |
center 1 kolumna, 2 wiersze W pierwszej komórce napis "Ankieta". |
| Web Forms / Label |
ID |
lblOpis |
| Web Forms / Repeater |
ID |
rptWyniki |
| Web Forms / Repeater |
ID |
rptAnkiety |
Kod wiążący powyższy interfejs z danymi będzie bardzo podobny do tego, który powstał dla komponentu ankieta_box. Dodatkowym elementem będzie wykorzystanie funkcji Ankieta_db.AnkietyLista(). Ponadto, wartość parametru IdAnkiety zostanie określona na podstawie parametru ankieta dołączonego do URL strony (np. Ankieta_wyniki.aspx?ankieta=d750d2cd-3ffb-4a6f-8970-4a9efe934444). W tym przypadku pokazałem także, jak można bindować poszczególne kontrolki niezależnie od siebie.
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
DoDataBind();
}
private void DoDataBind()
{
Guid IdAnkiety = new Guid(Request["ankieta"]);
Ankieta_db db = new Ankieta_db();
SqlDataReader rdrWyniki = db.AnkietyJedna(IdAnkiety);
if(rdrWyniki.HasRows)
{
rdrWyniki.Read();
lblOpis.Text = rdrWyniki["ank_opis"].ToString();
rdrWyniki.NextResult();
rptWyniki.DataSource = rdrWyniki;
rptWyniki.DataBind();
}
SqlDataReader rdrAnkiety = db.AnkietyLista(false);
rptAnkiety.DataSource = rdrAnkiety;
rptAnkiety.DataBind();
}
Niby wszystko w porządku, ale tak przygotowana strona nie wyświetli nam jeszcze żadnych wyników. Jak już wspomniałem wcześniej, kontrolka Repeater powiela zdefiniowany przez programistę fragment kodu HTML. Czy ktoś widział tu gdzieś programistę? Aaaa, no tak...
Kod HTML do powielenia przez Repeater musimy napisać ręcznie, nie pomoże nam w tym żaden kreator ani narzędzie do wizualnego projektowania. Po wyświetleniu projektu strony Ankieta_wyniki.aspxklikamy na znajdującą się na dole zakładkę oznaczoną etykietą HTML (obok jest zakładka Design, z której korzystaliśmy do tej pory projektując interfejsy kolejnych aplikacji).
W kodzie źródłowym strony z łatwością odszukamy następujące dwa fragmenty:
<TD>
<asp:repeater id="rptWyniki" runat="server">
</asp:repeater>
</TD>
[...]
<asp:Repeater id="rptAnkiety" runat="server">
</asp:Repeater>
Naszym zadaniem jest uzupełnienie deklaracji kontrolek rptWyniki i rptAnkiety o tzw. szablony (agn. Template) elementów. Będzie to wyglądało mniej-więcej w ten sposób:
<table border="0">
<asp:repeater id="rptWyniki" runat="server">
<ItemTemplate>
<tr>
<td><%# DataBinder.Eval(Container.DataItem, "opc_opis") %></td>
<td width="200">
<div style='<%# DataBinder.Eval(Container.DataItem, "opc_procent_max", "background-color:blue;width:{0}%") %>'></div>
</td>
<td>głosów:
<%# DataBinder.Eval(Container.DataItem, "opc_glosy") %>
</td>
<td>(<%# DataBinder.Eval(Container.DataItem, "opc_procent") %>%)</td>
</tr>
</ItemTemplate>
</asp:repeater>
</table>
[...]
<ul>
<asp:Repeater id="rptAnkiety" runat="server">
<ItemTemplate>
<li>
<a href='<%# DataBinder.Eval(Container.DataItem, "ank_id", "Ankieta_wyniki.aspx?ankieta={0}") %>'>
<%# DataBinder.Eval(Container.DataItem, "ank_opis") %>
</a>
</li>
</ItemTemplate>
</asp:Repeater>
</ul>
O co w tym chodzi?
- Łatwo zauważyć, że pierwszy repeater będzie powtarzał wiersze tabeli (
<tr>...</tr>), a drugi - elementy listy wypunktowanej (<li>...</li>). Z tego powodu każdy z repeaterów trzeba było dodatkowo objąć tagami HTML dopełniającymi te konstrukcje: w pierwszym przypadku był to znacznik TABLE, a w drugim UL.
- W niektórych miejscach pojawiły się dziwne elementy, zaznaczone żółtym kolorem. Zaczynają się charakterystycznym znacznikiem
<%# i kończą %>. Są to tzw. wyrażenia bindowania. Podobne do nich można zobaczyć w kodzie źródłowym stron we wszystkich miejscach, gdzie do tej pory określaliśmy bindowanie atrybutów za pomocą okna Properties, tylko że tam były generowane automatycznie. Zajmijmy się nimi nieco bardziej szczegółowo.
- Tam, gdzie wyrażeń bindowania używamy jako wartości atrybutów kontrolek, wokół nich są zastosowane apostrofy (') zamiast cudzysłowiów ("). Pozwala to uniknąć konfliktów z cudzysłowiami używanymi wewnątrz wyrażeń.
Wyrażenia bindowania
Gdy wiążemy jakiś atrybut kontrolki ze zmienną lub źródłem danych, informacje o tym powiązaniu są zapisywane w kodzie strony właśnie za pomocą wyrażeń postaci <%# ... %>. W najprostszym przypadku takie powiązanie mogłoby wyglądać tak: visible='<%# true %>' - to znaczy, że atrybut visible jakiejś kontrolki wiążemy ze stałą true, co powoduje, że jest ona zawsze widoczna. Żadna wielka filozofia, i bez tego przecież jest, a prościej napisać od razu visible="true".
Ciekawszy przykład wyrażeń bindowania pojawił się w kontrolce ankieta_box: tam atrybut Visible etykiety lblJuzGlosowal był związany z wyrażeniem <%# !MozeGlosowac %> - czyli ze zmienną klasy ankiet_box. Co jednak dzieje się wewnątrz naszych Repeaterów? Przeanalizujmy najciekawszy przykład, fragment kodu generujący wykresy słupkowe.
<div style='<%# DataBinder.Eval(Container.DataItem, "opc_procent_max", "background-color:blue;width:{0}%") %>'></div>
Widzimy element HTML o nazwie DIV (oznacza wydzielony fragment dokumentu), którego atrybut style jest przywiązany do wyrażenia
<%# DataBinder.Eval(Container.DataItem, "opc_procent_max", "background-color:blue;width:{0}%") %>.
DataBinder jest obiektem ułatwiającym wiązanie atrybutów do źródeł danych takich jak DataReader lub DataSet. Udostępnia on metodę Eval, przyjmującą dwa lub opcjonalnie trzy (jak powyżej) parametry. Pierwszym z nich jest źródło danych, tutaj Container.DataItem. W bieżącym kontekście kontenerem (tzn obiektem zawierającym) jest w tym przypadku obiekt typu RepeaterItem, generowany przez kontrolkę Repeater dla każdego rekordu swojego źródła danych. Jak łatwo zgadnąć, Container.DataItem odnosi się w tej sytuacji do bieżącego rekordu danych podczepionych do obiektu rptWyniki.
Kolejnym parametrem funkcji Eval jest wyrażenie określające ścieżkę do konkretnego elementu danych, jaki będzie wybierany ze źródła danych. Tutaj - opc_procent_max, czyli jedno z pól rekordu zwracanego z bazy danych przez procedurę AnkietyJedna.
Ostatni parametr - to format, w jakim będzie zwracana wybrana wartość. Jest to taki sam łańcuch formatujący, jak używany przez funkcję String.Format(), z którą mieliśmy okazję zapoznać się już w drugim odcinku kursu.
Po połączeniu ze sobą wszystkich powyższych informacji, można już powiedzieć, że gdy w bieżącym rekordzie wartość pola opc_procent_max przyjmie wartość 50, to na podstawie analizowanego fragmentu kodu Repeaterwygeneruje następujący tekst:
<div style='background-color:blue;width:50%'></div>
Tym sposobem utworzyliśmy całą kliencką część aplikacji. Pozostał nam do zrobienia interfejs administracyjny, składający się ze strony z listą ankiet oraz ze strony pozwalającej na edycję wybranej lub utworzenie nowej ankiety.
Admin - lista ankiet
Strona Ankieta_admin.aspx ma budowę bardzo zbliżoną do strony Ankieta_wyniki.aspx. Składa się z linku (Web Forms / HyperLink) o etykiecie "Nowa ankieta", prowadzącego do strony Ankieta_admin_ed.aspxoraz z repeatera generującego tabelę z listą ankiet. Nie będę tutaj szczegółowo analizował budowy tej strony, ponieważ powtarza ona rozwiązania omówione już wcześniej. Warto jedynie zaznaczyć, że:
- Przy każdej pozycji listy znajdują się dwa przyciski (Web Forms / LinkButton):
Ukryj/Pokaż i Usuń oraz link do formularza edycyjnego postaci Ankieta_admin_ed.aspx?ankieta=tutaj_guid_ankiety.
- Dla Repeatera
rptAnkiety określono dodatkowo szablon nagłówka (<HeaderTemplate> ... </HeaderTemplate>).
- Etykieta przycisku
Ukryj/Pokaż jest określana za pomocą troszkę bardziej złożonego wyrażenia:
<%# (bool)(DataBinder.Eval(Container.DataItem, "ank_ukryta"))?"Pokaż":"Ukryj" %>.
Konstrukcja warunek?wyrażenie_A:wyrażenie_B zwraca wyrażenie A lub B w zależności od wartości logicznej zadanego warunku. Rzutowanie na typ bool jest niezbędne, ponieważ funkcja Eval zwraca wartości typu Object.
- Przyciski
Ukryj/Pokaż oraz Usuń są obsługiwane przez jedną procedurę obsłgi zdarzenia ListaCommand. Powiązanie to jest zdefiniowane przez ustawienie atrybutu OnCommand="ListaCommand". Procedura ListaCommand rozpoznaje rodzaj klikniętego przycisku na podstawie jego atrybutu CommandName, a identyfikator rekordu - na podstawie atrybutu CommandArgument.
- Do obsługi przycisków
Ukryj/Pokaż i Usuń powstały w bazie danych dwie dodatkowe procedury składowane: AnkietyUkryj i AnkietyUsun (kod poniżej). Wraz z nimi, w komponencie dostępu do danych Ankieta_db pojawiły się dwa dodatkowe obiekty typu SqlCommand oraz dwie funkcje, skonstruowane analogicznie do funkcji AnkietyGlosuj()
. W ramach ćwiczenia ten fragment kodu proponuję spróbować napisać samodzielnie.
Oto obiecane procedury składowane:
CREATE PROCEDURE AnkietyUkryj @ank_id uniqueidentifier
AS
UPDATE Ankiety SET ank_ukryta = 1-ank_ukryta WHERE ank_id = @ank_id
CREATE PROCEDURE AnkietyUsun @ank_id uniqueidentifier
AS
DELETE FROM Ankiety WHERE ank_id = @ank_id
DELETE FROM Opcje WHERE opc_ankieta = @ank_id
Po przebrnięciu przez niezbyt odkrywczą stronę Ankiety_admin.aspx możemy się w końcu zabrać za edycję pojedynczej ankiety oraz za obiekt DataSet.
Złożone operacje - DataSet
Jak już wspomniałem, DataSet działa w odłączeniu od bazy danych. Można na nim wykonywać przeróżne operacje, a następnie zaaplikować do bazy danych wszystkie zmiany za jednym zamachem. Cecha ta sprawia, że obiekt ten jest idealny do edycji danych, zwłaszcza gdy obejmują one kilka zależnych od siebie tabel. W naszej aplikacji pozwoli to np. na określenie cech ankiety i utworzenie w niej wielu opcji bez "dotykania" serwera SQL i zapisanie całego pakietu rekordów na samym końcu, gdy już cała edycja zostanie zakończona. Jest to typowa sytuacja, w której użycie datasetu jest bardziej opłacalne, niż "kombinowanie" z obiektem DataReader i całą gromadką obiektów typu Command.
Podobnie jak w przypadku współpracy z obiektem DataReader, również para obiektów DataAdapeter i DataSet wymaga od nas przygotowania pewnego kodu zapewniającego interfejs z bazą danych. Żeby go przygotować, musimy zastanowić się, jakiego rodzaju dane będzie przechowywał nasz DataSet.
Ponieważ datasetu będziemy używali do edycji ankiety, to w miarę łatwo się domyślić, że będą się nań składały dwie tabele: Ankiety i Opcje. Skoro w danej chwili czasu poddajemy edycji tylko jedną ankietę, to i do datasetu będziemy ładowali dane ograniczone do jednej ankiety. Acha - to nam nasuwa pewną myśl. Mamy już procedurę składowaną zwracającą jedną kompletną ankietę oraz obiekt typu Command, korzystający z tej procedury. Jak pamiętamy ze wstępu do niniejszego artykułu, do wypełnienia tymi danymi datasetu brakuje nam jedynie obiektu typu DataAdapter, swoistej pompy ssąco-tłoczącej pośredniczącej pomiędzy bazą danych a obiektem DataSet. Zobaczmy, co uda nam się zrobić.
Komponent dostępu do danych
Po otworzeniu obszaru projektowego komponentu Ankieta_db, należy przeciągnąć nań z palety Data kontrolkę SqlDataAdapter. Poskutkuje to wyświetleniem Kreatora, prowadzącego nas krok po kroku przez proces tworzenia DataAdaptera. Na pierwszym ekranie należy kliknąć przycisk Next- pojawi się następujący formularz:

Wybieramy nasze połączenie robocze i klikamy Next.

Kolejny ekran pyta nas, w jaki sposób DataAdapter ma korzystać z bazy danych: za pomocą skonstruowanych przez programistę poleceń SQL, za pomocą nowych procedur składowanych, czy przy użyciu istniejących procedur. Wybieramy oczywiście ostatnią opcję, ponieważ mamy już przygotowaną odpowiadającą nam pod każdym względem procedurę AnkietyJedna. Następnie należy kliknąć Next.

Kolejnym krokiem jest wybranie procedury składowanej, z której będzie korzystał nowy DataAdapter. Dla polecenia Select wybieramy AnkietyJedna, natomiast pozostałe pozostawiamy puste. Ten DataAdapter będzie tylko wypełniał DataSet, korzystając w tym celu z procedury zwracającej dwa zbiory rekordów (przypominam: pierwszy, 1-elementowy, opisujący Ankietę, oraz drugi - zawierający listę dostępnych opcji). Do aktualizacji danych wygodniej korzystać z oddzielnych DataAdapterów dla każdej z tabel datasetu.
Po kliknięciu Next kreator wyświetli podsumowanie, a my możemy już kliknąć przycisk Finish. W tym momencie należy usunąć ewentualne nadmiarowe obiekty typu SqlCommand, które mogły powstać w procesie konfigurowania DataAdaptera, a następnie zmienić wartość jego atrybutu SelectCommand z czegoś w rodzaju SqlSelectCommand1 na cmdAnkietyJedna. Dlaczego sugeruję przeprowadzenie takiej dziwnej operacji? Kreator konfiguracji DataAdaptera wygenerował dla niego nowy obiekt typu SqlCommand reprezentujący procedurę składowaną AnkietyJednai nadał mu właśnie nazwę SqlSelectCommand1. Po co jednak mnożyć byty ponad potrzebę, skoro mamy już obiekt SqlCommand przypisany do tej procedury? Prosta podmiana, a znacznie zwiększa elegancję rozwiązania. Ponadto warto w tym momencie zmienić nazwę DataAdaptera np. na daAnkietaLadujDS (od ładowania ankiety do DataSetu).
Ktoś mógłby zapytać: W porządku, po co w takim razie korzystaliśmy z kreatora, skoro tak beztrosko pozbyliśmy się efektów jego pracy? Otóż, nie wszystkich. I to stwierdzenie prowadzi nas prosto do nowego pojęcia, jakim są mapowania tabel (ang. Table Mappings). Koncepcję tą wyjaśnia poniższy rysunek:

Dane pochodzące ze źródła, zwracane przez obiekt typu Command, na ogół są ponazywane w sposób niezbyt użyteczny i wygodny. O ile w przypadku serwera SQL mamy jeszcze w miarę komfortową sytuację, to nazwy jednostek danych pochodzących z egoztycznych magazynów mogą nijak nie nadawać się do normalnego użytku. Ponadto, konstruując datasety w aplikacji możemy chcieć trzymać się określonej konwencji, niekoniecznie spójnej z nazewnictwem w bazie danych. Ponieważ DataAdapter jest uniwersalnym pośrednikiem pomiędzy dowolnym źródłem danych a datasetem, to musi udostępniać mechanizm tłumaczenia, czy też mapowania, nazw zwracanych przez źródło na nazwy używane przez programistów w kontekście datasetu. Tym mechanizmem są właśnie mapowania.
Do tworzenia mapowań oczywiście nie potrzeba posługiwać się kreatorem konfiguracji DataAdaptera. Niemniej jednak, dla początkujących programistów ASP.NET jest to sposób nie tylko najszybszy, ale i najwygodniejszy. W dalszym ciągu możemy obejrzeć i/lub zmodyfikować wygenerowane automatycznie mapowania, korzystając z atrybutu TableMappings DataAdaptera. Po kliknięciu przycisku [...] znajdującego się przy tym atrybucie, zostanie wyświetlone okno dialogowe przedstawione na poniższym rysunku:

Jak widać, pierwszej ze źródłowych tabel (po nazwach pól widać, że to "Ankiety"), kreator przypisał nazwę tabeli datasetu taką samą, jak nazwa procedury składowanej (AnkietyJedna). Po wybraniu z listy rozwijanej Source Table drugiej tabeli można zobaczć, że odpowiadająca jej tabela datasetu otrzyma nazwę Table1. Taki bałagan nie bardzo nam odpowiada, dlatego zmieńmy nazwy w polu Dataset Tablena prawidłowe: Ankiety i Opcje. Ponieważ nazwy kolumn zostały zamapowane prawidłowo, to możemy już kliknąć OK i przejść do generowania DataSet-u.
Mając utworzony DataAdapter z prawidłowymi mapowaniami, DataSet tworzymy na jego podstawie za pomocą kilku kliknięć: prawym na DataAdapterze, lewym na poleceniu Generate Dataset, znów lewym na opcji New(w tym momencie trzeba wpisać nazwę tworzonego Datasetu, proponuję AnkietyDS) i jeszcze raz lewym - tym razem na przycisku OK. Gotowe. W Eksploratorze Projektu widzimy, że pojawił się nowy plik: AnkietyDS.xsd. Od raz zobaczmy, co też się w nim znajduje.

To, co widać na powyższym rysunku, to schemat struktury datasetu wygenerowany automatycznie na podstawie mapowań zdefiniowanych w DataAdapterze. Musimy go uzupełnić o dwa elementy: klucze główne w tabelach (klucze nie są automatycznie importowane) oraz definicję związku między tymi dwiema tabelami.
Aby utworzyć nowy klucz główny, należy kliknąć prawym przyciskiem myszy na definicji wybranej kolumny i wybrać polecenie Add / New Key. Po zaznaczeniu opcji Dataset primary keyklikamy przycisk OK.
Związek pomiędzy tabelami to coś, co jak sobie przypominamy, pominęliśmy podczas tworzenia struktury bazy danych. Tego, co teraz napiszę, nie widziałem jeszcze napisanego wprost w żadnej publikacji poświęconej ADO.NET - są to moje własne przemyślenia, wynikające z praktyki (podobnie jak używanie pól typu uniqueidentifierjako identyfikatorów w tabelach). Nie twierdzę, że nikt na te pomysły nie wpadł, po prostu trudno znaleźć tego typu rady w standardowej dokumentacji.
Baza danych udostępnia silny mechanizm wiązania ze sobą tabel, pozwalający zagwarantować spójność związków integralności za pomocą takich mechanizmów, jak na przykład automatyczne usuwanie powiązanych rekordów z tabel podrzędnych (u nas byłoby to automatyczne usuwanie opcji przy usuwaniu ankiety). Jeżeli mechanizm edycji danych w aplikacji jest oparty na datasecie, to należy zrezygnować z tych usług po stronie bazy danych, a zamiast tego zdefiniować odpowiednie powiązania w datasecie. Podejście to wynika ze sposobu aktualizacji danych na podstawie datasetu.
Po wypełnieniu datasetu danymi (za pośrednictwem DataAdaptera) dane pozostają do dyspozycji użytkownika, który może wykonywać na nich dowolne operacje - takie jak dodawanie, usuwanie i modyfikacja rekordów. Zmiany te nie są odzwierciedlane w bazie danych doputy, dopóki użytkownik lub logika aplikacji tego wyraźnie nie zażąda. Co się dzieje, gdy np. usuniemy z datasetu rekord odpowiadający ankiecie? Załóżmy na chwilę, że związki integralności są zdefiniowane w bazie, a nie ma ich w datasecie. Czyli: usuwany jest rekord z tabeli Ankiety (dataset), ale w tabeli Opcje pozostały rekordy podrzędne. Aplikacja rząda następnie aktualizacji bazy danych na podstawie datasetu. DataAdapter przegląda dataset i napotyka na rekord oznaczony jako usunięty, w wyniku czego wysyła do bazy danych polecenie fizycznego skasowania jego bazodanowego odpowiednika. Skutkuje to również wyrzuceniem z bazy rekordów potomnych - tutaj związki integralności działają prawidłowo. W efekcie otrzymujemy zupełnie rozsynchronizowany z bazą danych obiekt dataset.
Jeżeli zrobimy odwrotnie, definiując więzy integralności na poziomie datasetu, a pomijając je w bazie danych, to wszystko zadziała prawidłowo. Przy oznaczeniu rekordu ankiety w datasecie jako usuniętego, zostaną także automatycznie oznaczone odpowiednie rekordy opcji. Podczas aktualizacji bazy na podstawie datasetu, DataAdapter spokojnie usunie kolejno odpowiednie rekordy z odpowiednich tabel, bez oporu ze strony nadgorliwej bazy danych. Podejście to sprawdza się nawet przy bardzo skomplikowanych systemach i wielu zawiłych powiązaniach pomiędzy dziesiątkami tabel.
Aby utworzyć powiązanie pomiędzy naszymi tabelami, należy przeciągnąć pole klucza głównego tabeli Ankiety i upuścić je na tabeli Opcje (przeciągając chwytamy pole z lewej strony, tuż przy krawędzi tabelki). Zostanie wyświetlone okno dialogowe jak na poniższym rysunku:

W centralnej tabelce widać nazwę pola klucza głównego tabeli nadrzędnej (ank_id), pozostaje jedynie wybrać odpowiadające mu pole w tabeli podrzędnej - na liście należy odszukać pole opc_ankieta. Następnie można jeszcze ustawić regułę kasowania rekordów (Delete rule) na Cascade, jednak w przypadku naszej prostej bazy robimy to tylko "dla zasady" (jak pamiętamy, kasowanie ankiet odbywa się tu nie przez dataset, lecz przy użyciu procedury składowanej i obiektu typu Command, z poziomu listy na formularzu Ankiety_admin.aspx). Po kliknięciu OK można zapisać i zamknąć schemat datasetu.
Mamy już gotowy, skonfigurowany dataset, mamy także obiekt DataAdapter, który będzie go wypełniał. Brakuje nam jeszcze dwóch DataAdapterów, których zadaniem będzie aktualizowanie odpowiednich tabel w bazie na podstawie zmian w datasecie. Aby je utworzyć postępujemy podobnie jak poprzednio, z tą różnicą, że w drugim kroku kreatora DataAdaptera wybieramy opcję Create New Stored Procedures(utwórz nowe procedury składowane). W efekcie uzyskamy następujące okno dialogowe:

W głównym polu tej strony kreatora należy wpisać zapytanie SQL definiujące dane, jakie będzie obsługiwał konfigurowany DataAdapter. W naszym przypadku będzie to po prostu SELECT * FROM Ankiety. Następnie należy kliknąć przycisk Next. Kolejny ekran poprosi nas o podanie nazw dla tworzonych procedur składowanych:

Po wpisaniu wszystkich czterech nazw można kliknąć Next, a następnie Finish.
Jeżeli kreator nie zauważył, że może skorzystać z istniejącego już obiektu połączenia cnn i utworzył nowe, to niestety musimy je znów usunąć (to nowe, nie cnn) i zmodyfikować atrybut Connection obiektów typu SqlCommand skonfigurowanych wewnątrz nowego DataAdaptera: SelectCommand, DeleteCommand, InsertCommand i UpdateCommand - wszystkie one powinny korzystać z obiektu cnn. Po nadaniu DataAdapterowi porządnej nazwy - np. daAnkieta - możemy dla porządku zajrzeć mu w kolekcję TableMappings. Tym razem powinno tu być wszystko w porządku, ponieważ kreator ustalił nazwę tabeli na podstawie wpisanego przez nas zapytania. Można też odświeżyć listę procedur składowanych w panelu Server Explorer i obejrzeć wygenerowane automatycznie procedury pozwalające na aktualizację, dopisywanie i usuwanie danych w tabeli Ankiety.
Powyższe czynności należy powtórzyć dla tabeli Opcje. Kiedy już będziemy dysponowali kompletem obiektów: daAnkietaLadujDS, daAnkieta, daOpcja oraz poprawnym plikiem schematu datasetu (AnkietyDS.xsd), możemy w końcu przejść do pisania kodu, udostępniającego światu (tzn. reszcie aplikacji) zgrabne, wypełnione tym co trzeba, obiekty typu AnkietaDS. Otwórzmy plik Ankiety_db.cs w trybie edycji kodu. Poniżej - dwie procedury. Pierwsza wypełnia i zwraca wypełniony dataset, a druga zapisuje do bazy zmiany, które zaszły datasecie przekazanym do niej w formie parametru. Chyba już widać po co było to całe klikanie. To jest kompletny kod odpowiedzialny za komunikację datasetu z bazą danych!
public void LadujAnkietyDS(Guid ank_id, AnkietyDS ds)
{
ds.Clear();
daAnkietaLadujDS.SelectCommand.Parameters["@ank_id"].Value = ank_id;
daAnkietaLadujDS.Fill(ds);
}
public void ZapiszAnkietyDS(AnkietyDS ds)
{
daAnkieta.Update(ds.Ankiety);
daOpcja.Update(ds.Opcje);
}
W tym momencie mamy już kompletny komponent dostępu do danych, odpowiadający na nasze potrzeby związane z używaniem zarówno obiektów typu DataReader jak i DataSet. Korzystając z niego możemy pobierać dane, zapisywać je do bazy danych i wykonywać elementarne operacje, takie jak oddanie głosu na opcję lub ukrycie/odkrycie ankiety. Możemy zabrać się za formularz do edycji ankiety.
Interfejs użytkownika
Tworzenie strony Ankieta_admin_ed.aspxzaczniemy tradycyjnie od skonstruowania interfejsu użytkownika:

Troszkę niestandardowo, przed położeniem innych kontrolek na formularz, najpierw przeciągnijmy nań kontrolkę typu DataSet z palety Data. Rezultatem tego ruchu będzie wyświetlenie okienka, w którym należy wybrać utworzoną uprzenio definicję datasetu (AnkietyDS). Po nadaniu nowemu obiektowi nazwy (dsAnkiety) możemy przejść do umieszczania na formularzu pozostałych elementów interfejsu:
| Paleta/kontrolka | Atrybut | Wartość |
| Web Forms / HperLink |
Text NavigateUrl |
« Powrót do listy Ankieta_admin.aspx |
| HTML / Horizontal Rule |
|
|
| Web Forms / TextBox |
DataBindings
Width ID |
wiążemy atrybut Text z atrybutem DataSet-u: dsAnkiety.Ankiety.DefaultView.DefaultView[0].ank_opis 600px txtOpis |
| Web Forms / RequiredFieldValidator |
ErrorMessage ControlToValidate |
Opis wymagany txtOpis |
| HTML / Table |
Width układ |
600px jeden wiersz, 5 kolumn, w pierwszej napis "Od:", w trzeciej napis "Do:" |
| Web Forms / Calendar |
Formatowanie DataBinding
ID |
Wg uznania wiążemy atrybut SelectedDate z dsAnkiety.Ankiety.DefaultView.DefaultView[0].ank_start calStart |
| Web Forms / Calendar |
Formatowanie DataBinding
ID |
Wg uznania wiążemy atrybut SelectedDate z dsAnkiety.Ankiety.DefaultView.DefaultView[0].ank_koniec calKoniec |
| Web Forms / CheckBox |
Text DataBindings
ID |
Ukryta wiążemy atrybut Checked z dsAnkiety.Ankiety.DefaultView.DefaultView[0].ank_ukryta chkUkryta |
| Web Forms / DataGrid |
Formatowanie AutoGenerateColumns DataSource DataMember DataKeyField Width ID Columns |
Wg uznania false dsAnkiety Opcje opc_id 600px gridOpcje Opis w dalszej części tekstu |
| Web Forms / TextBox |
ID |
txtNowaOpcja |
| Web Forms / Button |
Text CausesValidation ID |
Dodaj false btnDodajOpcje |
| HTML / Horizontal Rule |
|
|
| Web Forms / Button |
Text ID |
Zapisz do bazy btnZapisz |
Zanim zajmiemy się kolumnami tabeli DataGrid zawierającej opcje ankiety, ustawmy na chwilę jej atrybut AutoGenerateColumns na truei przygotujmy kod wiążący cały formularz z danymi:
private void Page_Load(object sender, System.EventArgs e)
{
PobierzDane();
this.DataBind();
}
private void PobierzDane()
{
if(this.ViewState["DS"] == null)
{
if(Request.QueryString["ankieta"] != null)
{
Ankieta_db db = new Ankieta_db();
db.LadujAnkietyDS(new Guid(Request["ankieta"]), dsAnkiety);
}
else
{
dsAnkiety.Ankiety.AddAnkietyRow(
Guid.NewGuid(), "", DateTime.Today, DateTime.Today.AddDays(7), true);
}
this.ViewState["DS"] = dsAnkiety;
}
else
{
dsAnkiety = (AnkietyDS)this.ViewState["DS"];
dsAnkiety.Ankiety[0].ank_ukryta = chkUkryta.Checked;
dsAnkiety.Ankiety[0].ank_opis = txtOpis.Text;
dsAnkiety.Ankiety[0].ank_start = calStart.SelectedDate;
dsAnkiety.Ankiety[0].ank_koniec = calKoniec.SelectedDate;
}
}
No, tutaj się dużo dzieje. W miarę standardowa jest procedura PageLoad, natomiast Pobierz danewygląda już zupełnie inaczej. W tym miejscu dochodzi do głosu pewna decyzja projektowa, którą podjąłem, dotycząca przechowywania datasetu pomiędzy kolejnymi odświeżeniami strony.
Trudne decyzje cz. 2
Jak już pisałem, w przypadku całej biblioteki ADO.NET kluczowa jest umiejętność wybrania, w zależności od sytuacji, liniowego dostępu do danych przy użyciu obiektów DataReader i Command lub przechowania wycinka bazy w datasecie. Jeżeli już podjęliśmy tą decyzję, a wybór - jak tutaj - padł na dataset, to znaczy, że czas trudnych decyzji jeszcze się nie zakończył.
Pierwsza z nich dotyczy tego, czy dataset będzie w jakiś sposób przechowywany, czy może raczej odtwarzany przy każdym odświeżeniu strony. Każde z tych podejść ma swoje wady i zalety, ale bardziej istotne jest to, że każde z nich ma zastosowanie w innych sytuacjach. Tam, gdzie dataset będzie używany raczej do pobierania informacji, będziemy go prawdopodobnie odświeżać za każdym razem. W naszym przypadku, gdy ma on przechowywać kompletną ankietę wraz z opcjami w trakcie jej edycji, raczej nierozsądne byłoby zamazywanie jego zawartości danymi z bazy, czyli oryginalnymi, sprzed zmian dokonanych przez użytkownika. Korzystając z tego schematu musielibyśmy dodatkowo zapisywać dane do bazy przed ich pobraniem stamtąd, co nie ma żadnego sensu. Dlatego w tej sytuacji będziemy dataset przechowywać.
Jeżeli zapadła decyzja o przechowywaniu datasetu, to musimy się jeszcze zastanowić, gdzie go trzymać. Do wyboru mamy właściwie dwa miejsca: stan sesji oraz ViewState. O stanie sesji będę jeszcze mówił przy okazji któregoś z kolejnych odcinków, teraz warto tylko wspomnieć, że jest to mechanizm pozwalający na przechowanie obiektów lub wartości na serwerze przez cały czas trwania sesji użytkownika. Stan sesji najczęściej używa się np. do trzymania identyfikatorów użytkowników czy jakichś innych, niezbyt obciążających serwer danych - bo trzeba pamiętać, że każdy użytkownik korzystający z serwera i dorzucający swoje trzy grosze w postaci stanu sesji obciąża w ten sposób pamięć serwera. Dataset można przechowywać w stanie sesji, jednak należy robić to bardzo ostrożnie i tylko gdy dobrze rozumie się efekty takiego podejścia oraz jego wpływ na pracę serwera. Wykorzystanie mechanizmu ViewState nie obciąża tak bardzo serwera, w zamian zaś powoduje zwiększenie rozmiaru stron wysyłanych do klienta. Coś za coś. Ja tutaj zdecydowałem się na przechowanie danych przy użyciu ViewState, ale w ramach ćwiczenia proponuję podmienić wszystkie odwołania do this.ViewState na Session - też powinno działać. W tym przypadku należałoby jeszcze znaleźć jakieś dobre miejsce na pozbycie się tego obiektu z pamięci serwera, tj. ze stanu sesji - myślę, że najlepiej robić to przy ładowaniu pliku AnkietyAdmin.aspx, ponieważ tam w końcu prowadzą wszystkie drogi - i zapis danych, i powrót do listy.
Powrót do programu
Po przeczytaniu powyższej dygresji, zawartość procedury PobierzDane nie powinna wyglądać już tak tajemniczo. Jeżeli nie mamy jeszcze datasetu zapisanego w ViewState, to jest on wypełniany na podstawie bazy (gdy edytujemy ankietę) lub dodawany jest doń nowy rekord - ankieta (gdy zakładamy nową ankietę). Następnie rezultat jest rejestrowany w kolekcji ViewState.
Przy kolejnych odświeżeniach strony, dataset jest po prostu odczytywany z kolekcji ViewState, dodatkowo następuje tutaj aktualizacja jego zawartości na podstawie wartości kontrolek - chcemy, żeby dataset reagował na np. zmianę opisu czy której z dat niezależnie od akcji użytkownika - również np. w przypadku dodania czy usunięcia opcji ankiety. Gdyby ten fragment pominąć, to po dodaniu nowej opcji ankiety, instrukcja this.DataBind() zamazałaby ewentualne zmiany poczynione przez użytkownika w ustawieniach pozostałych kontrolek - pobierając po prostu początkowe, nie zmienione wartości z datasetu.
Edycja
Od tego miejsca wszystko jest już bardzo proste. Nową opcję dodamy w procedurze obsługi zdarzenia przycisku btnDodajOpcje:
private void btnDodajOpcje_Click(object sender, System.EventArgs e)
{
if(txtNowaOpcja.Text!="")
dsAnkiety.Opcje.AddOpcjeRow(Guid.NewGuid(), dsAnkiety.Ankiety[0], txtNowaOpcja.Text, 0, 0, 0);
txtNowaOpcja.Text = "";
this.ViewState["DS"] = dsAnkiety;
this.DataBind();
}
Jedyne, na co warto zwrócić tutaj szczególną uwagę, to sama linia z wywołaniem metody AddOpcjeRow. Podobną widzieliśmy już przy zakładaniu nowej ankiety - tam pojawiła się metoda AddAnkietyRow(). Skąd się te metody wzięły? Jak widać należą one do datasetu. Nic takiego jednak nie pisaliśmy, skąd nasz dataset wie o tym, że potrzebujemy metody, która nazywa się AddAnkietyRow()? Oto jedno z najpotężniejszych narzędzi oferowanych przez Visual Studio .NET - datasety o silnie określonym typie. W dalszym ciągu można się odwoływać do danych w datasecie tak: (Guid)dsAnkiety.Tables["Ankiety"].Rows[0].ItemArray[0] - jednak czy nie prościej napisać dsAnkiety.Ankiety[0].ank_id, gdzie ank_id jest już typu Guid? Nie tylko jest to wygodniejsze, ale i zabezpiecza przed popełnieniem błędów. Atrybuty datasetu są obsługiwane przez technologię IntelliSense, podpowiadającą programiście dostępne elementy klas i informacje o parametrach funkcji. DataSet o silnie określonym typie został wygenerowany automatycznie na podstawie schematu, który utworzyliśmy jakiś czas temu. Proponuję wpisać w edytorze "dsAnkiety.", nacisnąć Ctrl+spacja i przez chwilę zająć się eksploracją tego, co nam udostępnia środowisko. Koniec znanego programistom PHP czy ASP przedzierania się przez kolekcje czy tablice pól w zwróconym z bazy zestawie rekordów.
W powyższym kodzie utworzyliśmy nowy rekord w tablicy Opcje, podrzędnej w stosunku do tablicy Ankiety. Mechanizm IntelliSense bezproblemowo podpowiedział, jakich i jakiego typu parametrów potrzebuje metoda AddOpcjeRow do utworzenia tego rekordu. Trudno sobie wyobrazić wygodniejsze rozwiązanie.
Po dodaniu kilku opcji przy użyciu naszego edytora pewnie chcielibyśmy dodać trochę funkcjonalności do tabeli DataGrid z opcjami - np. edycję czy usuwanie poszczególnych pozycji. Wróćmy na chwilę do trybu projektowania strony. Po kliknięciu prawym przyciskiem myszy kontrolki DataGrid i wybraniu polecenia Property Builder, zostanie wyświetlone okno przedstawione na poniższym rysunku:

Najbardziej interesuje nas grupa opcji Columns, pozwalająca na dokładne zdefiniowanie zawartości tabeli DataGrid:

Po ponownym wyłączeniu opcji Create columns automatically at run time (odpowiadającej atrybutowi AutoGenerateCollumns kontrolki DataGrid) możemy samodzielnie i bardzo szczegółowo określić które kolumny, i w jaki sposób, będą umieszczone w tabeli. Proponuję, żeby były to kolejno: opc_opis, opc_glosy i opc_procent. Ponadto, na liście Available columns są dostępne dwie bardzo szczególne kolumny, umieszczone w grupie Button Column: Edit, Update, Cancel oraz Delete. Dodajmy je także do listy Selected columns.
Po wybraniu kolumn można kliknąć kolejno każdą z nich na liście Selected columns i ustalić ich atrybuty, takie jak np. tekst nagłowka (Header text), opcję "tylko-do-odczytu" (Read only) - to w przypadku kolumn opc_glosy i opc_procent - oraz format wyświetlania (Data formatting expression), który przyda się zwłaszcza w przypadku kolumny opc_procent: {0} %. Następnie można przejrzeć pozostałe zakładki okna dialogowego i zapoznać się z dostępnymi możliwościami formatowania tabeli DataGrid. Trzeba przyznać, że są dość imponujące, a w zestawieniu z funkcją autoformatowania (prawy klik na kontrolce DataGrid i polecenie Auto Format)pozwalają na szybkie i wygodne przygotowanie eleganckich zestawień.
Po dokonaniu tych wszystkich operacji związanych z ustaleniem zawartości i formatu tabeli gridOpcje, powinniśmy uzyskać widok zbliżony do przedstawionego na zrzucie ekranu, kilka naciśnięć PageUpwyżej. Do pełnego działania kontrolki DataGrid konieczne jest jeszcze tylko oprogramowanie jej zdarzeń, związanych z akcjami użytkownika:
// Przejście do edycji wiersza
private void gridOpcje_EditCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
gridOpcje.EditItemIndex = e.Item.ItemIndex;
gridOpcje.DataBind();
}
// Anulowanie edycji wiersza
private void gridOpcje_CancelCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
gridOpcje.EditItemIndex = -1;
gridOpcje.DataBind();
}
// Aktualizacja datasetu po edycji wiersza
private void gridOpcje_UpdateCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
TextBox opisText = (TextBox)e.Item.Cells[0].Controls[0];
dsAnkiety.Opcje[e.Item.ItemIndex].opc_opis = opisText.Text;
gridOpcje.EditItemIndex = -1;
gridOpcje.DataBind();
}
// Usunięcie wiersza
private void gridOpcje_DeleteCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
dsAnkiety.Opcje.RemoveOpcjeRow(dsAnkiety.Opcje[e.Item.ItemIndex]);
gridOpcje.EditItemIndex = -1;
gridOpcje.DataBind();
}
Na co warto zwrócić uwagę:
- Procedury gridOpcje_EditCommand, gridOpcje_CancelCommand i gridOpcje_DeleteCommand są w miarę oczywiste.
- W procedurze gridOpcje_UpdateCommand musimy najpierw "dobrać się" do automatycznie wygenerowanego pola tekstowego, w którym użytkownik miał możliwość poddać edycji opis opcji. Jest to zawsze kontrolka o indeksie 0 w odpowiedniej komórce bieżącego wiersza DataGrid-u. Zawartość tego pola tekstowego przepisujemy do datasetu.
- Uwaga! Tutaj, przy aktualizacji i usuwaniu danych, mogliśmy indeksować dataset używając wartości e.Item.ItemIndex, zawierającej po prostu numer porządkowy bieżącego wiersza DataGrid-u. Jest to bezpieczne, ponieważ w naszym prostym przykładzie kolejność wierszy w tej kontrolce wprost wynika z kolejności wierszy w tabeli datasetu. Jeżeli korzysta się jednak z zaawansowanych funkcj kontrolki DataGrid, takich jak sortowanie lub dzielenie rekordów na strony, to wcale nie musi już być tak różowo i trzeba zastosować bardziej zaawansowane techniki wybierania odpowiedniego rekordu z datasetu. Tym i innymi zagadnieniami związanymi z obiektem DataGrid zajmiemy się w jednym z kolejnych odcinków kursu - kontrolka ta posiada tak ogromne możliwości, że warto poświęcić im cały jeden artykuł. To co dzisiaj przedstawiłem to dopiero wierzchołek góry lodowej.
Do oprogramowania pozostał nam jedynie przycisk odpowiadający za zapisanie zmian w datasecie do bazy danych. Jest to oczywiściezaskakująco proste:
private void btnZapisz_Click(object sender, System.EventArgs e)
{
Ankieta_db db = new Ankieta_db();
db.ZapiszAnkietyDS(dsAnkiety);
Response.Redirect("Ankieta_admin.aspx");
}
Podsumowując: obiekt DataSet, zwłaszcza w połączeniu z kontrolką DataGrid, daje bardzo duże możliwości. 90% pracy z tymi dwoma obiektami to ich konfigurowanie, za pomocą myszki, kolejnych kreatorów i okien dialogowych. Ilość kodu, który trzeba napisać żeby osiągnąć dość zaawansowaną funkcjonalność jest zaskakująco mała. Nawet te osoby, które nie lubią programować za pomocą myszki (ja się do nich zaliczam), z pewnością docenią prostotę tych rozwiązań i znaczny wzrost produktywności, jaki można dzięki nim osiągnąć.
Inne bazy danych
Często zdarza się, że aplikację trzeba połączyć z innym źródłem danych niż MS SQL Server lub MSDE. Prawie zawsze można do tego celu wykorzystać standard jakim jest ODBC. Jak to zrobić - pokażę na przykładzie serwera MySQL. Sterownik ODBC dla tego serwera można pobrać ze strony internetowej jego producenta.
Podobnie jak w przypadku serwera MS SQL, należy zacząć od kliknięcia Add Connection w panelu Server Explorer. Łącząc się z s serwerem firmy Microsoft korzystaliśmy jedynie z domyślnie wybranej zakładki Połączenie, w przypadku ODBC musimy na chwilę cofnąć się do poprzedniej - Dostawca - i wybrać źródło danych Microsoft OLE DB Provider for ODBC Drivers, a następnie kliknąć przycisk Dalej. Zakładka Połączenieprzyjmuje nową formę:

Wybieramy opcję Użyj ciągu połączenia i klikamy przycisk Konstruuj. W okienku, które się pojawi, wybieramy zakładkę Komputerowe żródło danych, prezentującą źródła ODBC skonfigurowane do użytku w danym komputerze i wskazujemy źródło ODBC utworzone wcześniej dla naszej bazy MySQL lub klikamy przycisk Nowe. Dalej postępujemy zgodnie ze wskazaniami kreatora, wybierając w odpowiednim momencie sterownik ODBC MySQL. Po utworzeniu źródła danych zostanie wyświetlone okno dialogowe konfiguracji sterownika ODBC, w którym należy podać nazwę źródła, nazwę lub adres IP serwera, nazwę bazy danych istniejącej na serwerze SQL oraz nazwę i hasło użytkownika. Przycisk Test Data Source pozwala sprawdzić, czy wszystkie parametry zostały podane prawidłowo. Jeżeli tak - klikamy OK aż do momentu powrotu do okna dialogowego Właściwości łącza danych. W tym momencie możemy wypełnić pozostałe parametry, jak widać na powyższym zrzucie ekranu, kliknąć Testuj połączenie i, jeżeli wszystko jest w porządku, OK. Po wykonaniu tych operacji możemy cieszyć się nowym połączeniem z serwerem MySQL:

Z połączenia ODBC korzysta się dalej w podobny sposób, jak z połączenia z SQL Serverem, używając po prostu obiektów OdbcConnection, OdbcCommand i OdbcDataAdapter zamiast znanych nam już klas Sql-owych. Ponadto trzeba pamiętać o tym, że nie wszystkie bazy danych obsługują mechanizm procedur składowanych (np. MySQL tego nie potrafi, lub nie potrafił gdy się nim więcej zajmowałem kilka lat temu). W takiej sytuacji trzeba korzystać z zapytań SQL wprost, nie przechowując ich w bazie danych - ustawiając wartość atrybutu CommandType obiektu odbcCommand na Text, i wpisując treść zapytania bezpośrednio do atrybuty CommandText (np. select * from tabelka).
Przy korzystaniu z datasetu, od momentu załadowania go danymi z OdbcDataAdaptera, postępowanie w żaden sposób nie różni się od opisanego w niniejszym artykule. Wszystkie różnice w dostępie do baz danych wyrównują obiekty typu DataAdapter. Nic nawet nie stoi na przeszkodzie, aby w jednym datasecie przechowywać dane pochodzące i z serwera MS SQL, i z MySQL - ładowane dwoma oddzielnymi, odpowiednimi dla danej bazy danych, adapterami.
Dodatek - typy kolumn w MS SQL Server/MSDE
Poniższa lista zawiera zestawienie typów kolumn dostępnych w serwerze MS SQL / MSDE wraz z krótkimi opisami.
| Typ pola | Opis |
| bigint |
Liczby całkowite z przedziału od -2^63 (-9223372036854775808) do 2^63-1 (9223372036854775807). Rozmiar pola - 8 bajtów. |
| binary |
Dane binarne o stałej długości (n bajtów, n z przedziału od 1 do 8000). Rozmiar pola - n+4 bajty. |
| bit |
Pole przechowujące wartość logiczną (0/1). Rozmiar pola - 1 bajt, z tym że bajt mieści do 8 pól typu bit (np. 9 pól typu bit zajmuje 2 bajty) |
| char |
Dane znakowe o stałej długości (n znaków, n z przedziału od 1 do 8000). Rozmiar pola - n+4 bajty. |
| datetime |
Data i czas od 1 stycznia 1753 do 31 grudnia 9999, z precyzją do 0,33 milisekundy. Rozmiar pola - 8 bajtów. |
| decimal |
Przechowuje liczby dziesiętne z określoną przez użytkownika precyzją. Rozmiar pola zależy od założonej precyzji. |
| float |
Liczby zmiennoprzecinkowe o określonym przez użytkownika rozmiarze. |
| image |
Dane binarne o dowolnym rozmiarze. Powyżej 8000 bajtów wymagają specjalnej obsługi. |
| int |
Liczby całkowite z przedziału od -2,147,483,648 do 2,147,483,647. Rozmiar pola - 4 bajty. |
| money |
Przechowuje dane monetarne (4 miejsca dziesiętne) o wartościach od -922,337,203,685,477.5808 do +922,337,203,685,477.5807. Rozmiar pola - 8 bajtów. |
| nchar |
Dane znakowe Unicode o stałej długości (n 2-bajtowych znaków, n z przedziału od 1 do 4000). Rozmiar pola - n*2+4 bajty. |
| ntext |
Dane tekstowe unicode o dowolnym rozmiarze. Powyżej 8000 bajtów wymagają specjalnej obsługi. |
| numeric |
Patrz typ decimal (typy ekwiwalentne). |
| nvchar |
Dane znakowe o zmiennej długości, maksymalnie n 2-bajtowych znaków (n z przedziału od 1 do 4000). Rozmiar pola - ilość wpisanych znaków*2+4. |
| real |
Liczby zmiennoprzecinkowe z przedziału od –3.40E + 38 do 3.40E + 38. Rozmiar pola 4 bajty. |
| smalldatetime |
Data i czas od 1 stycznia 1900 do 6 czerwca 2079, z precyzją do minuty. Rozmiar pola - 4 bajty. |
| smallint |
Liczby całkowite z przedziału od -32,768 do 32,767. Rozmiar pola - 2 bajty. |
| smallmoney |
Przechowuje dane monetarne (4 miejsca dziesiętne) o wartościach od -214,748.3648 do 214,748.3647. Rozmiar pola - 4 bajty. |
| sql_variant |
Typ danych, który jest w stanie przechować dane dowolnego typu obsługiwanego przez serwer MS SQL, za wyjatkiem text, ntext, image, timestamp i sql_variant. |
| text |
Dane tekstowe o dowolnym rozmiarze. Powyżej 8000 bajtów wymagają specjalnej obsługi. |
| timestamp |
Typ danych zawierający automatycznie generowane liczby binarne, których unikalność jest zagwarantowana w obrębie bazy danych. Każda zmiana dokonana względem rekordu powoduje zmianę wartości pola typu timestamp, dzięki czemu jest ono idealne do np. wykrywania modyfikacji rekordu. Rozmiar pola - 8 bajtów. |
| tinyint |
Liczby całkowite z przedziału od 0 do 255. Rozmiar pola - 1 bajt. |
| uniqueidentifier |
Globalnie unikalny identyfikator (GUID). Rozmiar pola - 16 bajtów. |
| varbinary |
Dane binarne o zmiennej długości, maksymalnie n bajtów (n z przedziału od 1 do 8000). Rozmiar pola - ilość wpisanych bajtów + 4. |
| varchar |
Dane znakowe o zmiennej długości, maksymalnie n znaków (n z przedziału od 1 do 8000). Rozmiar pola - ilość wpisanych znaków + 4. |
Podsumowanie
Tym sposobem dotarliśmy do końca dzisiejszego odcinka. Mam nadzieję, że udało mi się pokazać nie tylko jak korzystać z poszczególnych rozwiązań biblioteki ADO.NET, ale również w jaki sposób wybrać najlepsze z nich w danej sytuacji oraz jak zaplanować strukturę aplikacji, aby korzystanie z danych było jak najwygodniejsze i najefektywniejsze. Następny odcinek kursu będzie w większej części poświęcony kontrolkom DataGrid i DataList, chyba że na forum w miarę szybko pojawią się protesty. Do następnego odcinka!
Dodatkowe zasoby
Autor: Michał Chaniewski
Spis treści