Janusz Górczyński Wprowadzenie do programowania baz danych w VB.NET WSZiM w Sochaczewie, 2011
Spis treści 1 WSTĘP DO ADO.NET...4 1.1 KOMPONENTY ADO.NET...4 1.2 CONNECTION STRING...6 1.3 OBIEKT CONNECTION...7 1.3.1 SqlConnection...7 1.3.2 OleDbConnection...7 1.4 OBIEKT COMMAND...7 1.4.1 Metoda ExecuteReader...8 1.4.2 Metoda ExecuteScalar...8 1.4.3 Metoda ExecuteNonQuery...8 1.5 OBIEKT DATAREADER...9 1.5.1 Metoda Read...9 1.5.2 Metoda NextResult...10 1.5.3 Metoda GetSchemaTable...12 1.6 OBIEKT DATAADAPTER...13 1.6.1 DataAdapter, polecenie Select...13 1.6.2 DataAdapter, polecenia modyfikujące...14 1.6.3 DataAdapter i CommandBuilder...14 1.7 PROCEDURY PRZECHOWYWANE W VB.NET...15 1.7.1 Procedura bezargumentowa...15 1.7.2 Procedura z parametrami...17 1.8 WYKORZYSTYWANE ŹRÓDŁA DANYCH....19 1.9 STRUKTURA APLIKACJI...20 1.10 DODATKOWE REFERENCJE...21 1.11 MODUŁ WSPÓLNY...23 1.12 KLASA CMOJEOLE...23 1.13 OPIS POZYCJI OLEDB W MENU...29 1.13.1 Adapter...29 1.13.2 DataReader...32 1.13.3 Przegląd tabel...34 1.14 OPIS POZYCJI ADO (COM) W MENU...38 1.14.1 Pobieranie tabel i pól, test zapytania...38 1.14.2 Zbudowanie dynamicznego formularza...50 2
1.15 OPIS POZYCJI SQL...57 1.15.1 Adapter...57 1.15.2 Update tabeli via procedura przechowywana (klasycznie)...62 1.15.3 Update tabeli via procedura przechowywana wg JG...68 1.16 KLASA CFORSTORAGESUB...74 1.16.1 Deklaracja zmiennych i prywatnych procedur klasy...74 1.16.2 Kilka wybranych metod publicznych...77 3
1 Wstęp do ADO.NET ADO.NET jest opracowaną przez Microsoft technologią dostępową do baz danych, jest to jeden z ważnych składników środowiska.net Framework. Technologia ta zapewnia komunikację z relacyjnymi i nierelacyjnymi źródłami danych poprzez zestaw komponentów. ADO.NET zabezpiecza zarówno model połączeniowy ze źródłem danych jak i model bezpołączeniowy. W przypadku modelu połączeniowego (Connection Oriented Data Access Architecture) aplikacja zestawia połączenie ze źródłem danych, a następnie interaktywnie współpracuje z tym źródłem wykorzystując zapytania języka SQL. W tej technologii połączenie ze źródłem danych cały czas pozostaje otwarte, także wtedy, gdy aplikacja nie wykonuje żadnych operacji na źródle danych. W modelu bezpołączeniowym (Disconnected Data Access Architecture) połączenie jest zestawianie jedynie na moment, kiedy dane są pobierane ze źródła i umieszczane w obiekcie typu DataSet lub zwracane do źródła danych. 1.1 Komponenty ADO.NET Poniżej pokazany jest schemat funkcjonowania dostępu do baz danych z wykorzystaniem modelu ADO.NET (źródło: http://vb.net-informations.com). 4
Dwoma kluczowymi komponentami ADO.NET jest Data Provider oraz DataSet. Pierwszy z nich odpowiada za zestawienie połączenia ze źródłem danych, a drugi za reprezentowanie danych pobranych ze źródła (z bazy danych). Środowisko.Net Framework zabezpiecza trzy główne rodzaje provaiderów dla modelu ADO.NET: Microsoft SQL Provider obsługuje dostęp do baz danych MS SQL Server; OLEDB pozwala na dostęp do innych baz danych i innych źródeł danych; ODBC pozwala na tworzenie aplikacji z jednoczesnym dostępem do różnych typów danych. W przypadku zestawiania połączenia z MS SQL Server używamy obiektu SqlConnection, do innych baz danych OleDbConnection, a w przypadku korzystania z ODBC korzystamy z obiektu OdbcConnection. Cztery obiekty wchodzące w skład komponentu DataProvider zabezpieczają jego funkcjonalność. Są to obiekty: Connection odpowiada za fizyczne zestawienie połączenie ze źródłem danych; Command odpowiada za wykonanie zapytania skierowanego do źródła danych, może to być jawne polecenie SQL lub procedura przechowywana, zarówno wybierające jak i akcyjne (modyfikujące informacje w bazie danych); DataReader wyspecjalizowany obiekt do pobierania strumienia danych typu tylko do odczytu i do przeglądania jedynie w przód; DataAdapter obiekt pośredniczący w dostarczenie danych pobranych ze źródła do obiektu DataSet. Obiekt DataSet zabezpiecza bezpołączeniowy dostęp do danych pobranych ze źródła, jest tym samym całkowicie niezależny od tego źródła (poza momentem, gdy dane są pobierane z tego źródła). Obiekt DataSet może zawierać kolekcje obiektów DataTable, z których każdy jest wirtualnym odpowiednikiem danych pozyskanych ze źródła danych. Inną ważną kolekcją jest kolekcja relacji wiążąca obiekty DataTable. Obiekt DataTable zawiera kolekcję wierszy, kolekcję kolumn oraz kolekcję warunków nakładanych na poszczególne pola. Poniżej pokazany jest schemat obiektu DataSet. 5
1.2 Connection String Pod tym pojęciem rozumiemy informację tekstową zawierającą dane niezbędne do zestawienia połączenia ze źródłem danych. W zależności od użytego provaidera connection string ma następującą składnię: Microsoft SQL Server Connection String connetionstring ="Data Source = ServerName; Initial Catalog = Databasename; User ID = UserName; Password=Password" OLEDB Data Provider Connection String connetionstring = "Provider = Microsoft.Jet.OLEDB.4.0; Data Source = yourdatabasename.mdb;" ODBC Connection String connetionstring = "Driver = {Microsoft Access Driver (*.mdb)}; DBQ = yourdatabasename.mdb;" 6
1.3 Obiekt Connection 1.3.1 SqlConnection Obiekt SqlConnection odpowiada za utworzenie fizycznego połączenia aplikacji z bazą danych serwera SQL. Instancja klasy SqlConnection otrzymuje jako argument łańcuch połączenia (connection string). Po zestawieniu (otwarciu) połączenia mogą być wykonywane polecenia pobrania czy modyfikacji danych w źródle danych. Poniżej jedna z możliwych instrukcji deklaracji i utworzenia instancji obiektu SqlConnection, kolejna otwiera połączenie z bazą danych. Dim conn As New SqlConnection(connection_string) conn.open() Utworzona instancja obiektu Connection nie jest automatycznie zamykana, nawet po wyjściu z procedury, w której nastąpiło zestawienie połączenia. Musimy pamiętać o tym, aby po zakończeniu operacji na bazie danych zamknąć otwarte połączenie, wystarczy skorzystać z metody Close. Przypisanie wartości Nothing jest sygnałem do zwolnienia zasobu (do jego usunięcia z pamięci RAM komputera). conn.close() conn = Nothing 1.3.2 OleDbConnection Obiekt OleDbConnection odpowiada za utworzenie fizycznego połączenia aplikacji z bazą danych wskazaną w łańcuchu połączenia. Instancja klasy SqlConnection otrzymuje jako argument łańcuch połączenia (connection string). Po zestawieniu (otwarciu) połączenia mogą być wykonywane polecenia pobrania czy modyfikacji danych w źródle danych. Instrukcje deklaracji, utworzenia, otwarcia i zamknięcia połączenia są podobne do przedstawionych wyżej, cała różnica dotyczy użycia obiektu OleDbConnection zamiast SqlConnection. 1.4 Obiekt Command Obiekt Command odpowiada za wykonanie jawnego zapytania SQL lub procedury przechowywanej. Wymaga instancji obiektu Connection, otwarcia połączenia do źródła danych, a następnie przypisania go do właściwości Connection obiektu Command. Jeżeli wykonane zapytanie lub procedura przechowywana zwracają dane, to obiekt DataReader jest używany do ich odebrania z obiektu Command. Istotne właściwości tego obiektu to CommandText, który reprezentuje tekst zawierający polecenie do wykonania lub nazwę procedury przechowywanej oraz właściwość 7
CommandType, która określa rodzaj polecenia. Może to być jawne polecenie SQL, procedura przechowywana, może być to także tabela. Polecenie wykonywane jest po wywołaniu jednej z trzech metod, które udostępniane są przez obiekt Command. 1.4.1 Metoda ExecuteReader Metoda ta używana jest do pobrania ze źródła danych takiego ich zestawu, który chcemy tylko i wyłącznie przeglądać i to jedynie w przód. Zaletą jest szybkość działania tej metody. Wywołanie tej metody dostarcza dane do obiektu DataReader, którego nie można utworzyć programowo w kodzie (chodzi o to, że obiekt tego typu powstaje jako efekt wywołania metody ExecuteReader). Poniżej przykład fragmentu kodu, w którym tworzony jest obiekt DataReader. Dim dt As New DataTable Try conn.open() Dim oledbcmd As New OleDbCommand( Select * From Klienci, conn) Dim oledbreader As OleDbDataReader = oledbcmd.executereader dt.load(oledbreader) 1.4.2 Metoda ExecuteScalar Metoda ta wykorzystywana jest w tych sytuacjach, w których zapytanie SQL lub procedura przechowywana zwraca pojedynczy wynik z bazy danych. Dokładnie rzecz biorąc metoda ExecuteScalar zwraca wynik z pierwszego pola pierwszego rekordu. Przykładowy fragment kodu wykorzystujący tę metodę pokazany jest niżej. Dim conn As New SqlConnection(connetionString) Try conn.open() Dim cmd As New SqlCommand( Select count(*) From Klienci, conn) Dim count As Integer = CInt(cmd.ExecuteScalar()) Cmd = Nothing conn.close() conn = Nothing 1.4.3 Metoda ExecuteNonQuery Metoda uruchamia procedurę akcyjną typu insert (wstawienie rekordu), delete (usunięcie) lub update (aktualizację). Poniższy fragment kodu ilustruje użycie tej metody do usunięcia rekordu o danej wartości klucza. Dim conn As New OleDbConnection(connetionString) Try 8
conn.open() Dim cmd As New OleDbCommand( Delete From Klienci Where idk = 1, _ conn) cmd.executenonquery() conn.close() conn = Nothing 1.5 Obiekt DataReader Jak wcześniej już powidziełem obiekt DataReader pozwala na bardzo szybkie pobranie z bazy danych rekordestu, który możemy dalej wykorzystać np. jako źródło danych do takich obiektów jak ListBox, ComboBox czy DataGridView. Obiekt DataReader udostępnia trzy ważne metody: Read, NextResult oraz GetShemaTable. 1.5.1 Metoda Read Metoda Read pozwala na przeczytanie zawartości tego obiektu po pobraniu danych (czyli rekordów) odczytając wskazane pola przy pomiocy właściwości Item o podanym indeksie. Musimy pamiętać o tym, że po utworzeniu tego obiektu wskaźnik rekordów ustawiany jest na zerwej pozycji i że możemy te rekordy odczytywać tylko w przód (od zerowego do ostatniego indeksu). Oczywiście nie ma możliwości modyfikacji zestawu rekordów. Poniżej kod procedury odczytującej zawartość obiektu DataReader (procedura ta jest wmontowana w formularz frmoledbdatareader jako reakcja na klik przycisku o nazwie btnmetodaread). Imports System.Data.OleDb Private Sub btnmetodaread_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnmetodaread.click ' dekarujemy i tworzymy obiekt typu OLEDBConnetion Dim conn As New OleDbConnection(strconnOle) ' reszta operacji może spowodować błąd, dlatego umieszcamy je w Try-Catch Try ' otwieramy połączenie conn.open() ' deklarujemy i tworzymy obiekt typu OleDBCommand ' przekazując do niego zapytanie SQL oraz otwarte połączenie Dim oledbcmd As New OleDbCommand("select * from Pracownicy", conn) ' deklarujemy obiekt typu DBReader i wypełniamy go danymi ' poprzez wywołanie metody ExecuteReader obiektu OleDBCommand Dim oledbreader = oledbcmd.executereader ' czytamy obieky oledbreader Dim i As Integer, txt As String, j As Integer = 1 9
While oledbreader.read txt = "Rekord nr " & j.tostring & ": " For i = 0 To oledbreader.fieldcount - 1 txt &= oledbreader.item(i) & "; " Next MsgBox(txt.Substring(0, txt.length - 2), _ MsgBoxStyle.Information, _ "Ilustaracja metody Read obiektu DataReader") j += 1 End While oledbcmd = Nothing MsgBox("Wszystkie rekordy odczytane", MsgBoxStyle.Information, _ "Ilustaracja metody Read obiektu DataReader") Catch ex As Exception MsgBox("Problem z wykonaniem polecenia", _ MsgBoxStyle.Critical, _ "Ilustaracja metody Read obiektu DataReader") Finally conn.close() conn = Nothing End Try End Sub 1.5.2 Metoda NextResult Metoda NextResult może być wykorzystywana w tych zapytaniach, w których zależy nam na zwróceniu więcej niż jednego zestawu rekordów przy jednym połączeniu z bazą danych. Poniżej kod procedury zaimplementowanej w formularzu frmoledbdatareader jako reakcji na klik przycisku o nazwie btnmetodanextresult). Imports System.Data.SqlClient Private Sub btnmetodanextresult_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnmetodanextresult.click Dim sql1, sql2 As String sql1 = "select * from Klienci;" sql2 = "select * from RodzajSal" ' dekarujemy i tworzymy obiekt typu SqlConnetion Dim conn As New SqlConnection(strconnSqL) ' reszta operacji może spowodować błąd, dlatego umieszcamy je ' w Try-Catch Try ' otwieramy połączenie conn.open() 10
' deklarujemy i tworzymy obiekt typu sqlcommand ' przekazując do niego zapytanie SQL oraz otwarte połączenie Dim sqlcmd As New SqlCommand(sql1 & sql2, conn) ' deklarujemy obiekt typu DBReader i wypełniamy go danymi ' poprzez wywołanie metody ExecuteReader obiektu OleDBCommand Dim sqlreader = sqlcmd.executereader ' czytamy obieky oledbreader Dim i As Integer, txt As String, j As Integer = 1 While sqlreader.read txt = "Rekord nr " & j.tostring & ": " For i = 0 To sqlreader.fieldcount - 1 txt &= sqlreader.item(i) & "; " Next MsgBox(txt.Substring(0, txt.length - 2), _ MsgBoxStyle.Information, "Tabela Klienci") j += 1 End While ' urucgamiamy pobranie rekordsetu z drugiego polecenia sqlreader.nextresult() ' bedziemy czytać rekordy, ustawiamy zmienną j na 1 j = 1 While sqlreader.read txt = "Rekord nr " & j.tostring & ": " For i = 0 To sqlreader.fieldcount - 1 txt &= sqlreader.item(i) & "; " Next MsgBox(txt.Substring(0, txt.length - 2), _ MsgBoxStyle.Information, "Tabela RodzajSal") j += 1 End While sqlcmd = Nothing MsgBox("Wszystkie rekordy odczytane z obu tabel", _ MsgBoxStyle.Information, _ "Ilustaracja metod Read i NextResult obiektu DataReader") Catch ex As Exception MsgBox("Problem z wykonaniem polecenia", _ MsgBoxStyle.Critical, _ "Ilustaracja metod Read i NextResult obiektu DataReader") Finally conn.close() conn = Nothing End Try End Sub 11
1.5.3 Metoda GetSchemaTable W momencie utworzenia obiektu DataReader na podstawie zapytania sformułowanego w zapytaniu możemy pobrać informacje o kolumnach tego zapytania poprzez wywołanie metody GetSchemaTable. Metoda ta zwraca obiekt typu DataTable, który zawiera tyle wierszy, ile jest zwracanych w zapytaniu. Z pomocą zagnieżdżonych pętli For Each można przejrzeć kolekcję wierszy i kolumn obiektu DataTable odczytując nazwy właściwości poszczególnych kolumn tabeli bazy danych, do których odnosi się zapytanie oraz ich wartości. Poniżej kod procedury zaimplementowanej w formularzu frmoledbdatareader jako reakcji na klik przycisku o nazwie btngetschematable). Imports System.Data.SqlClient Private Sub btngetschematable_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btngetschematable.click ' dekarujemy i tworzymy obiekt typu SqlConnetion Dim conn As New SqlConnection(strconnSqL) ' reszta operacji może spowodować błąd, dlatego umieszcamy je w Try-Catch Try ' otwieramy połączenie conn.open() ' deklarujemy i tworzymy obiekt typu sqlcommand ' przekazując do niego zapytanie SQL oraz otwarte połączenie Dim sqlcmd As New SqlCommand("select * from Klienci", conn) ' deklarujemy obiekt typu DBReader i wypełniamy go danymi ' poprzez wywołanie metody ExecuteReader obiektu OleDBCommand Dim sqlreader = sqlcmd.executereader ' tworzymy obiekt DataTable i pobieramy schemat tabeli Dim schematable As DataTable = sqlreader.getschematable() pomocnicze zmienne dla odczytania schematu Dim row As DataRow Dim column As DataColumn ' przeglądamy wiersz po wierszu obiekt DataTable ' każdy wiersz opisuje jedną kolumnę tabeli bazy danych For Each row In schematable.rows Dim txt As String = "" For Each column In schematable.columns txt &= column.columnname & ",: " & row(column).tostring & _ vbcrlf Next MsgBox(txt) Next sqlreader.close() 12
Catch ex As Exception MsgBox("Problem z wykonaniem polecenia", MsgBoxStyle.Critical, _ "Ilustracja metody GetSchemaTable DataReader") Finally conn.close() conn = Nothing End Try End Sub 1.6 Obiekt DataAdapter Obiekt DataAdapter zabezpiecza komunikację między obiektem DataSet i źródłem danych (DataSource). DataAdapter pozwala, dzięki metodzie Fill, na dostarczenie danych do obiektu DataSet (dokładniej do obiektu DataTable, który jest tworzony w DataSet). Inne metody obiektu DataAdapter pozwalają także na komunikację odwrotną, czyli na wprowadzanie, aktualizację czy usuwanie danych w źródle (w DataSource). 1.6.1 DataAdapter, polecenie Select Poniżej fragment kodu realizującego zadanie pobrania danych ze źródła i umieszczeniu ich w obiekcie typu DataSet. Imports System.Data.SqlClient ' inne potrzebne instrukcje Dim conn As New SqlConnection(strconn) Dim ds As New DataSet, i As Integer Try conn.open() Dim adapter As New SqlDataAdapter("select * from Klienci", conn) adapter.fill(ds) For i = 0 To ds.tables(0).rows.count - 1 MsgBox(ds.Tables(0).Rows(i).Item(1)) Next Catch ex As Exception MsgBox(ex.ToString) Finally conn.close() conn = Nothing End Try 13
1.6.2 DataAdapter, polecenia modyfikujące Poniżej fragment kodu procedury, której zadaniem jest wprowadzenie rekordu do tabeli WykazSal, która zawiera trzy pola: ids jako pole typu integer z automatyczną inkrementacją o jeden (jest kluczem tej tabeli), NrSali, pole typu tekstowego o długości do 5 znaków oraz LiczbaMiejsc, pole typu integer Imports System.Data.SqlClient Private Sub WprowadzRekord(ByVal strconn As String, _ ByVal txtnumer As String, ByVal Miejsc As Integer) Dim conn As New SqlConnection(strconn) Dim adapter As New SqlDataAdapter Dim sql As String sql = "Insert into WykazSal (NrSali, LiczbaMiejsc) values( " & _ NrSali & ", " & LiczbaMiejsc & ")" Try conn.open() adapter.insertcommand = New SqlCommand(sql, conn) adapter.insertcommand.executenonquery() MsgBox("Wiersz wstawiony") Catch ex As Exception MsgBox(ex.ToString) Finally conn.close() conn = Nothing End Try End Sub 1.6.3 DataAdapter i CommandBuilder Z obiektem DataAdapter wykonującym zapytanie wybierającje wszystkie pola z danej tabeli (czyli polecenie select) współpracuje obiekt CommandBuilder, który potrafi wygenerować polecenia delete, update i insert na podstawie zapytania wybierającego. Wygenerowane zapytania aktualizujące można wykorzystać do aktualizacji danych w źródle danych. Poniżej fragment kodu ilustrujący takie działanie. Imports System.Data.OleDb Dim conn As New OleDbConnection(constring) Dim oledbcmdbuilder As OleDbCommandBuilder Dim ds As New DataSet, i As Integer Try conn.open() Dim oledbadapter As New OleDbDataAdapter( _ select * From Pracownicy, conn) oledbcmdbuilder = New OleDbCommandBuilder(oleDbAdapter) oledbadapter.fill(ds) 14
With ds.tables(0) For i = 0 To.Rows.Count - 1.Rows(i).Item(2) &= ".pl" Next End with oledbadapter.update(ds.tables(0)) MsgBox("Adresy mailowe uzupełnione") Catch ex As Exception MsgBox(ex.ToString) Finally conn.close() conn = Nothing End Try 1.7 Procedury przechowywane w VB.NET Bardzo silną stroną serwera SQL jest możliwość zaprojektowania w bazie danych utworzonej na tym serwerze zapytania napisanego w języku Transact-SQL, które następnie jest kompilowane i dopiero w takiej postaci wykonywane. Fakt komilacji zapytania znakomicie przyspiesza jego wykonanie i to jest pierwsza korzyść z procedur przechowywanych. Inna, nie mniej ważna związana jest z bezpieczeństwem bazy danych, a związana jest z faktem, że można tak skonfigurować uprawnienia, aby możliwość manipulowania danymi była jedynie za pośrednictwem procedur przechowywanych. Dzięki temu nie ma żadnej możliwości wykonania jawnego kodu SQL. Procedura przechowywana może być różnego typu, zarówno wybierająca jak i akcyjna. Z reguły będziemy wykorzystywać procedury z argumentami, zarówno wchodzącymi jak i wychodzącymi. Mogą się także zdarzyć procedury bezargumentowe, raczej wybierające niż akcyjne. 1.7.1 Procedura bezargumentowa Powiedzmy, że w bazie danych, do której będziemy się odwoływać istnieje tabela Klienci. Powiedzmy dalej, że chcemy z niej odczytać identyfikator klienta oraz jego nazwę. Procedura przechowywana może mieć następującą konstrukcję: Create procedure dbo.ppobierzdane As Select idk, Nazwa From Klienci order by Nazwa Po stronie aplikacji (VB.NET) pobranie tych danych i przypisanie ich jako źródło danych do obiektu typu ComboBox tak, aby pole Nazwa było wyświetlane w rozwijanej liście, 15
a pole idk było zwracane po wybraniu nazwy klienta w liście może być zrealizowane za pomocą niżej pokazanych instrukcji. Imports System.Data.SqlClient Dim conn As New SqlConnection(strconn) Dim cmd As New SqlCommand ' deklarujemy i tworzymy instancję obiektu dataset Dim ds As New DataSet Dim i As Integer ' reszta działań może spowodować błądm stąd użycie bloku ' Tray-Catch Try conn.open() ' przypisanie do właściowości Connection obiektu cmd ' otwartego połączenia cmd.connection = conn ' określenie typu polecenia cmd.commandtype = CommandType.StoredProcedure ' podanie nazwy procedury przechowywanej cmd.commandtext = "dbo.ppobierzdane" ' przygotowania do odebrania danych, które zwróci procedura ' deklarujemy obiekt typu DataReader Dim dr As SqlDataReader ' poprzez wywołanie metody ExecuteReader dostarczamy dane ' do DataReader dr = cmd.executereader ' deklarujemy i tworzymy obiekt typu Dataset Dim dt As New DataTable ' wywołujemy metodę Load utworzonego obiektu wskazując jako ' źródło danych obiekt dr (DataReader) dt.load(dr) ' tworzymy źródło danych dla cboklienci (ComboBox) With Me.cboKlienci.DataSource = dt.displaymember = Nazwa.ValueMember = idk End With Catch ex As Exception MsgBox(ex.ToString) Finally conn.close() conn = Nothing End Try 16
1.7.2 Procedura z parametrami Tak jak wspomniałem wcześniej w większości przypadków procedura przechowywana będzie posiadała parametry (argumenty). Przy jej wywoływaniu z aplikacji musimy utworzyć kolekcję parametrów. Dla każdego z nich musimy określić takie elementy jak nazwa parametru, jego typ, kierunek, rozmiar czy wartość. Powiedzmy, że mamy inną procedurę w naszej bazie, ta również zwraca klientów, ale będziemy ich dodatkowo rozróżniać z uwagi na wartość pola StatusKlienta, jest to pole typu integer. Create procedure dbo.ppobierzklientawgstatusu @statusk int As Select idk, Nazwa From Klienci where StatusKlienta = @statusk order by Nazwa Poniżej pokazany jest fragment kodu wywołujący tę procedurę, zwrócony zestaw rekordów będzie źródłem danych dla formantu typu ListBox. Użytkownikowi pokażemy nazwę klienta (pole Nazwa), po wyborze pozycji z listy chcemy otrzymać wartość pola idk. Będziemy pobierali listę tych klientów, dla których pole StatusKlienta ma wartość np. 2. Imports System.Data.SqlClient Dim conn As New SqlConnection(strconn) Dim cmd As New SqlCommand ' deklarujemy i tworzymy instancję obiektu dataset Dim ds As New DataSet Dim i As Integer ' reszta działań może spowodować błądm stąd użycie bloku ' Tray-Catch Try conn.open() ' przypisanie do właściowości Connection obiektu cmd ' otwartego połączenia cmd.connection = conn ' określenie typu polecenia cmd.commandtype = CommandType.StoredProcedure ' podanie nazwy procedury przechowywanej cmd.commandtext = "dbo.ppobierzdane" ' zerujemy kolekcję parametrów cmd.parameters.clear() ' deklarujemy nowy parametr Dim param As SqlParameter param = New SqlParameter("@idk", SqlDbType.Int, 0) 17
param.value = 2 ' określamy, czy parametr jest wchodzący czy wychodzący param.direction = ParameterDirection.Input ' dodajemy utworzony parametr do kolekcji parametrów cmd.parameters.add(param) Dim dr As SqlDataReader dr = cmd.executereader Dim dt As New DataTable dt.load(dr) ' tworzymy źródło danych dla lstklienci (ListBox) With Me.lstKlienci.DataSource = dt.displaymember = Nazwa.ValueMember = idk End With Catch ex As Exception MsgBox(ex.ToString) Finally conn.close() conn = Nothing End Try W aplikacji ADOAndADONET zainteresowany Czytelnik znajdzie wiele przykładów wykorzystania modelu ADO.NET jak i starszego modelu ADO do przetwarzania informacji zgromadzonych w bazach danych. 18
Aplikacja ADOandADONET Aplikacja została przygotowana w celu zademonstrowania podstawowych metod modelu ADO.NET oraz jego poprzednika ADO w zakresie dostępu do baz danych typu MS Access oraz MS SQL Server. 1.8 Wykorzystywane źródła danych. Aplikacja korzysta z bazy danych TestSP.mdb zawierającej trzy przykładowe tabele o nazwach Studenci, Pracownicy i Przedmioty. Definicje tych tabel pokazane są niżej. Dla zademonstrowania współpracy z bazą SQL wykorzystywana będzie tabela o nazwie Klienci i definicji jak niżej. Tabela ta może być umieszczona w dowolnej bazie danych na lokalnym czy zdalnym serwerze MS SQL Server. Istotne jest, abyśmy mieli prawo modyfikowania danych w tej tabeli. Będziemy także korzystać z trzech procedur przechowywanych, ich definicje pokazane są niżej. Pierwsza z nich pobiera wszystkie dane z tej tabeli (instrukcją select), druga wykonuje instrukcję update (aktualizacji), a ostatnia odpowiada za wstawienie danych nowego klienta (instrukcja insert). Create procedure [dbo].[pdajdaneklientow] as select * from dbo.klienci order by Nazwa 19
Create procedure [dbo].[pupdateklienci] @idk int, @nazwa nvarchar(100), @adres nvarchar(100), @nip nvarchar(13), @mail nvarchar(50), @telefon nvarchar(50), @osoba nvarchar(100) as update dbo.klienci set Nazwa=@nazwa, Adres=@adres, NIP=@nip, mail=@mail, telefon=@telefon, OsobaKontaktowa=@osoba where idk=@idk Create procedure [dbo].[pwstawklienta] @naz nvarchar(100),@adres nvarchar(100), @nip nvarchar(13), @mail nvarchar(50), @tel nvarchar(50), @osoba nvarchar(100), @idk int out as insert into dbo.klienci (Nazwa, Adres, NIP, mail, telefon, OsobaKontaktowa) values (@naz, @adres, @nip, @mail, @tel, @osoba) set @idk=scope_identity() 1.9 Struktura aplikacji Aplikacja została zaprojektowana jako wieloformularzowa, zawiera formularz główny o nazwie frmmdiform, będzie on pełnił rolę kontenera dla pozostałych formularzy. Dodano do niego formant MenuStrip w celu zbudowania menu aplikacji. Poniżej widok projektu tego formularza z projektem menu. Jako główne pozycje w pasku menu umieszczono: SQL będzie zawierał polecenia związane z wykorzystaniem modelu ADO.NET do współpracy z bazą MS SQL Server; 20
OLEDB tu będą zgromadzone polecenia demonstrujące różne aspekty wykorzystania modelu ADO.NET do manipulowania danymi przykładowej bazy danych (TestSP.mdb); ADO (COM) dwa polecenia podrzędne w tej grupie pokażą jak można korzystać z starego modelu ADO do uzyskiwania informacji o strukturze bazy danych (o tabelach i polach wybranej tabeli). Będzie tu także pokazany sposób dynamicznego zbudowania formularzy ekranowych dla tabeli o nieznanej wcześniej strukturze (w pewnym sensie jako ciekawostka programistyczna). Do rozwiązania dodano plik modułu wspólnego o nazwie ADOWspolny.vb, będzie on zawierał deklaracje stałych i zmiennych o charakterze publicznym dla projektu, czyli takich, które muszą być dostępne dla pozostałych klas wykorzystywanych w tym rozwiązaniu. W projekcie umieszczono autorską klasę CForStorageSub zawierającą szereg metod ułatwiających dostęp do procedur przechowywanych oraz klasę CTestSQL jako jej klasę pochodną. Instancje klasy CTestSQL będą odpowiedzialne za wykonanie poleceń związanych z pozycją SQL w menu aplikacji. Klasa CMojeOle definiuje metody i właściwości związane z wykonywaniem poleceń związanych z pozycją OLEDB w menu aplikacji. 1.10 Dodatkowe referencje. Z uwagi na zamiar wykorzystywania modelu ADO musimy dodać do naszego rozwiązania referencje do odpowiednich bibliotek (jak pokazano niżej). 21
Po ich dodaniu lista referencji naszego rozwiązania powinna wyglądać tak, jak jest to pokazane niżej (ważne, aby były tam dwie pokazane wcześniej pozycje: adodb oraz Microsoft ADO Ext. 6.0 for DDL and security). 22
1.11 Moduł wspólny Jak wspomniałem wcześniej mamy tu deklaracje stałych i zmiennych publicznych, czyli takich, które muszą być dostępne we wszystkich innych obiektach tej aplikacji. 1.12 Klasa CMojeOle Klasa ta przeznaczona jest do obsługi baz danych typu MS Access, dlatego pierwsze jej dwie instrukcje importują odpowiednie przestrzenie nazw. W kodzie klasy zadeklarowano trzy zmienne prywatne, będą one wykorzystywane przez metody tej klasy. Widoczny jest także konstruktor bezparametrowy tej klasy. 23
Kolejny konstruktor (procedura publiczna o nazwie New) odpowiada za połączenie ze źródłem danych wskazanym informacją zawartą w argumencie strconn oraz wykonanie polecenia SQL przekazanego argumentem strsql. Kolejny konstruktor New będzie odpowiadał za powiązanie formularza zawierającego etykiety i pola tekstowe odpowiadające konkretnej tabeli bazy danych z danymi pobranymi z tej tabeli. Zakładamy, że nazwy pól tekstowych odpowiadających polom tej tabeli zaczynają się od prefiksu txt, jest to bardzo ważne, jeżeli mamy je powiązać z danymi (inaczej będzie błąd czasu wykonania). Z tego warunku wynika także ograniczenie na nazwy pól bazy danych nazwy te nie mogą zawierać spacji!. 24
Kolejny konstruktor będzie tworzył instancję klasy CMojeOle wykorzystywaną do pobrania ze źródła danych informacji, które mają być dostarczone do formantu typu ListBox jako jego źródło danych z jednoczesnym wskazaniem, jakie informacje mają być widoczne dla użytkownika oraz jaka informacja ma być zwrócona w momencie kliku danej pozycji listy. Z reguły w formularzu zawierającym formanty typu ListBox, CheckedListBox czy ComboBox tworzona jest procedura umożliwiające pobranie informacji o wskazanej przez użytkownika pozycji. Procedura taka obsługuje zdarzenie SelectedIndexChange, a jest wywoływana nie tylko w momencie kliku wybranej pozycji, lecz także w momencie definiowania źródła danych takiego formantu. W celu zablokowania takiej niepożądanej reakcji wykorzystamy zmienną publiczną flaga ustawiając jej wartość na False na czas definiowania źródła danych. 25
W klasie CMojeOle utworzyłem jeszcze jeden konstruktor, instancja klasy utworzona z jego pomocą buduje obiekt typu DataTable będący połączeniem dwóch tabel. Konstruktor został wymyślony po to, aby można było uzyskać źródło danych dla formantu typu ComboBox, gdzie na pierwszej pozycji będzie pozycja opisująca symbolicznie wszystkie pozostałe pozycje. Z reguły polega to na umieszczeniu na początku listy informacji typu (wszystko) czy (uwzględniając wymogi języka polskiego) (wszyscy) lub (wszystkie). Wybranie takiej pozycji jest wtedy odpowiednikiem zapytania select lista_kolumn from nazwa_tabeli. Z kolei wybranie innej pozycji jest odpowiednikiem zapytania jak wyżej, ale uzupełnionego warunkiem where. Przedstawiony niżej konstruktor realizuje przygotowanie odpowiedniego źródła danych. Przy okazji jego kod pokazuje, jak można manipulować takim obiektem jak DataTable poprzez 26
dodawanie nowych kolumn i wierszy, a także dołączenie do jednej tabeli innej tabeli o takiej samej strukturze. 27
Uzupełnieniem kodu klasy CMojeOle są jeszcze trzy procedury typu Property zwracające wartości wybranych zmiennych prywatnych. Poniżej widok diagramu UML klasy CMojeOle, jest na nim widoczna metoda o nazwie MergeDataTable, została ona zastąpiona przez ostatni z przedstawionych konstruktorów jako bardziej uniwersalne rozwiązanie. W grupie Methods widzimy informację o konstruktorach, o tym, że istnieje w sumie pięć konstruktorów (czyli mamy do czynienia z przeciążeniem metod). 28
1.13 Opis pozycji OLEDB w menu 1.13.1 Adapter Wskazana pozycja menu otrzymała nazwę mnuapapterpracownicy, a jej klik spowoduje wykonanie pokazanej niżej procedury. Procedura deklaruje i tworzy nowy egzemplarz formularza frmoleadaptercmdb. W kolejnej instrukcji do zmiennej publicznej strcostam przypisywane jest zapytanie wybierające z tabeli Pracownicy wszystkie rekordy zawierające komplet pól. Treść tego zapytania jest także przypisywana do tytułu utworzonego formularza. Ostatnie dwie instrukcje określają formularz nadrzędny dla utworzonego formularza oraz nakazują jego wyświetlenie. Formularz frmoleadaptercmd jest stosunkowo prosty w sensie użytych formantów, zawiera bowiem tylko dwa formanty o nazwach: dgvdane formant typu DataGridView, będziemy w nim wyświetlać pobrane dane; btnaktualizuj przycisk polecenia, jego klik wywoła procedurę zdarzeniową odpowiedzialną za aktualizację danych. 29
Znacznie ciekawszy jest kod tego formularza. 30
Klik przycisku btnaktualizuj uruchamia poniższą procedurę. A tak wygląda ten formularz w trakcie pracy, do ostatniego wiersza w polu Adres dopisano dwa końcowe znaki. Klik przycisku Aktualizuj zmodyfikował dane w bazie danych. 31
1.13.2 DataReader Polecenia zebrane w tej pozycji menu demonstrują sposób pobrania danych z bazy w celu zbudowania źródła danych dla takiego formantu jak ListBox (analogicznie będzie dla ComboBox). Klik podpozycji Studenci (nazwa mnudrstudenci) uruchamia poniższą procedurę (w frmmdiform.vb). Pierwsza instrukcja tej procedury deklaruje i tworzy nowy egzemplarz formularza o nazwie frmoledatareader. Formularz ten zawiera jeden formant typu ListBox o nazwie cbopracownik. Do zmiennej globalnej strcostam przypisywane jest zapytanie zwracające dwie kolumny danych, pierwsza z nich zwraca identyfikator studenta, drugie kombinację dwóch pól (nazwisko i imię). Istotne są tutaj nazwy tych kolumn, w przypadku pierwszej będzie to nazwa pola, dla drugiej kolumny tworzymy nazwę (tu jest to Kto ). Kolejna instrukcja deklaruje i tworzy nowy egzemplarz klasy CMojeOle wykorzystując odpowiedni kontruktor. W efekcie tworzone i otwierane jest połączenie z bazą danych, za pomocą obiektu DataReader odczytywane są potrzebne dane i przypisywane do formantu listy. Jeżeli wszystko przebiegło poprawnie, to własność Komunikat jest pustym ciągiem znaków. Wykorzystując warunek If Then Else End if badamy, czy operacja pobrania danych i zbudowania źródła danych dla cbopracownik przebiegła poprawnie. Jeżeli tak, to pokazujemy formularz na ekranie, jak nie, to wyświetlamy stosowny komunikat. W kodzie formularza frmoledatareader mamy tylko jedną procedurę, jej zadaniem jest zwrócenie informacji o identyfikatorze wybranej przez użytkownika pozycji listy. Warto zwrócić uwagę na pierwszą instrukcję tej procedury, jest tu badanie, czy zdarzenie ma być obsługiwane czy też nie. Jeżeli zmienna globalna flaga nie jest True, to opuszczamy procedurę. 32
Tradycyjnie na zakończenie tego podrozdziału zrzut ekranowy pokazujący pracę tego formularza. 33
1.13.3 Przegląd tabel Ta pozycja menu OLEDB daje dostęp do trzech poleceń odpowiedzialnych za wyświetlenie indywidualnie zaprojektowanych formularzy dla tabel Pracownicy, Studenci i Przedmioty. Każdy z tych formularzy wyposażony jest w zestaw przycisków pozwalających na poruszanie się po rekordach tych tabel. Podpozycja Pracownicy w menu Przegląd tabel jest identyfikowana poprzez nazwę mnuppracownicy, a jej klik uruchamia pokazaną niżej procedurę. Pierwsza instrukcja tej procedury deklaruje i tworzy instancję (egzemplarz) formularza frmpracownicy (jego projekt będzie pokazany za chwilę). Kolejna instrukcja przypisuje do zmiennej prywatnej sql treść zapytania zwracającego wszystkie rekordy z tabeli Pracownicy. Deklarowany i tworzony jest egzemplarz klasy CMojeOle wykorzystujący jeden z konstruktorów tej klasy. Przekazujemy do niego łańcuch połączenia, treść zapytania oraz utworzony przed chwilą formularz. W klasie nastąpi otwarcie połączenia, pobranie potrzebnych danych, zwrócenie ich do obiektu typu DataSet oraz ustanowienie połączenia między polami 34
tekstowymi formularza a odpowiadającymi im kolumnami w tabeli o nazwie Test w DataSet. Utworzony obiekt DataSet będzie dostępny poprzez właściwość DajDS tej klasy. Jeżeli wszystko przebiegło poprawnie, to zmienna globalna mydataset otrzymuje potrzebny obiekt z instancji klasy, tworzony jest tytuł formularza i na końcu pokazujemy formularz. Gdyby coś poszło nie tak, to zamiast formularza wyświetlany jest stosowny komunikat. Poniżej pokazany jest projekt formularza frmpracownicy. Na jego powierzchni umieszczono pola tekstowe (TextBox) odpowiadające polom tabeli Pracownicy. Kolejno są to: txtid tu będzie wyświetlany identyfikator pracownika txtnazwisko jego nazwisko; txtimie i imię; txtadres oraz adres. Pola te poprzedzone są etykietami (Label), których właściwości Text zostały tak skomponowane, aby odpowiadały poszczególnym polom. Poniżej tych formantów umieszczono cztery przyciski o nazwach odpowiednio btnpierwszy, btnnastepny, btnpoprzedni oraz btnostatni. Właściwości Text tych przycisków zostały dostosowane do ich przeznaczenia, którym jest wymuszenie przejścia do wskazanego rekordu. Między tymi przyciskami umieszczono jeszcze jedno pole tekstowe, jego nazwa to NrRekordu (nie może zaczynać się od txt, bo wtedy była by próba skojarzenia tego pola z kolumną w obiekcie DataSet, a takiej kolumny nie ma). W polu tym będziemy wyświetlać numer bieżącego rekordu. 35
W kodzie klasy formularza frmpracownicy znajdziemy wiele ciekawych procedur. Prywatna procedura KtoryRekord ustala, jaki jest numer bieżącego rekordu, a następnie wyświetla go w polu tekstowym NrRekordu. Jej kolejnym zadaniem jest ustalenie, które z przycisków nawigacyjnych mają być dostępne, a które nie. Procedura obsługująca załadowanie formularza wywołuje tylko tą prywatną procedurę KtoryRekord. Kolejno budujemy procedury obsługujące poruszanie się po rekordach. 36
Tradycyjnie dwa zrzuty ekranowe tego formularza w pracy. 37
1.14 Opis pozycji ADO (COM) w menu Znajdziemy tu dwa polecenia, tak jak pokazano to niżej. Pierwsze z nich jest dostępne i pozwala na wskazanie pliku bazy danych MS Access w wersji 2003 (plik z rozszerzeniem *.mdb), jego otwarcie, pobranie listy tabel, a dla wybranej tabeli listy jej pól. Będziemy także mogli zbudować zapytanie SQL i zobaczyć efekt jego działania. Drugie z poleceń jest chwilo niedostępne, a będzie udostępnione dopiero po wskazaniu pliku bazy danych w pierwszym z pokazanych poleceń. Polecenie to jest ciekawe programistycznie, pokazuje bowiem jak programowo (dynamicznie) można budować formularze. 1.14.1 Pobieranie tabel i pól, test zapytania Klik tego polecenia powoduje wykonanie pokazanej niżej procedury. Jak widzimy nic ciekawego tu nie, pierwsza instrukcja tworzy instancję klasy formularza frmtestsql, a dwie kolejne odpowiadają za wyświetlenie formularza. Formularz frmtestsql jest dość skomplikowany, zawiera dwie listy, pole tekstowe do wyświetlenia treści zapytania, formant typu DataGridView do wyświetlenia wyników zapytania oraz kilka przycisków poleceń i etykiet opisujących niektóre formanty. Projekt formularza wraz z nazwami i przeznaczeniem poszczególnych formantów pokazany jest niżej. 38
lsttabele formant typu ListBox, będzie wyświetlał tabele wybranej bazy danych; lstpola także lista, będą tu pokazywane nazwy pól wybranej tabeli; txtsql pole tekstowe (TextBox), wyświetlimy w nim zapytanie. Jego właściwość Multiline została ustawiona na True, a wysokość formantu na 45 pkt (tak, aby można było pokazać dwa wiersze); dgvdane formant typu DataGridView, tu będziemy prezentować wyniki zapytania; btnaddwhere przycisk polecenia (Button), uruchamia dodanie do zapytania nazwy tabeli i słowa kluczowego Where; btnwykonaj przycisk uruchamiający procedurę odpowiedzialną za wykonanie zapytania; btnclearsql przycisk polecenia usuwający wszelkie informacje z pola txtsql. W kodzie klasy tego formularza umieszczono dość dużą liczbę procedur i funkcji odpowiedzialnych za jego funkcjonowanie. Kolejno będę je prezentował. 39
Zaczynamy od zaimportowania niezbędnych przestrzeni nazw (do współpracy z bazą danych MS Access. Pierwszą ważną funkcją w tej klasie jest GetFileName, funkcja odpowiedzialna za wyświetlenie okna dialogowe typu OpenFileDialog i pobranie od użytkownika pełnej nazwy pliku bazy danych (nazwy i ścieżki do niego wiodącej). Procedura obsługujące zdarzenia Load formularza korzysta z funkcji GetFileName. Jeżeli użytkownik wskazał plik bazy danych, to wykorzystując obiekt ADOX modelu ADO pobierane są informacje o tabelach tej bazy i umieszczane w liście lsttabele. 40
Ustalana jest liczna tabel dla wybranej bazy, ścieżka dostępu do niej umieszczane jest w zmiennej globalnej strconndynamic, a polecenie Zbudowanie dynamicznego formularza zostaje udostępnione. 41
Pokazana niżej procedura wykonywana jest w momencie, gdy użytkownik wybrał jedną z tabel. Musimy wtedy przejrzeć kolekcję tabel tej bazy, znaleźć tę tabelę, która została wybrana, a następnie pobrać listę pól dla tej wybranej tabeli. Nazwa pola (kolumny) może (choć lepiej byłoby gdyby nie) zawierać spację. W zapytaniu taka nazwa musi być opakowana w nawiasy kwadratowe. Pokazana niże funkcja realizuje właśnie to zadanie. 42
Kolejna procedura reaguje na klik jednej z pozycji w liście lstpola. Jej zadaniem jest skomponowanie treści zapytania wybierającego umieszczonego w polu txtsql. Klik przycisku Dodaj Where uruchamia procedurę, której zadaniem jest sprawdzenie, czy dotychczas skonstruowane zapytanie zawiera frazę From, jeżeli nie, to taka fraza jest dodawana do zapytania, następnie dodawana jest nazwa tabeli pobrana z lsttabele (czyli wybór użytkownika). Do sprawdzenia, czy w zapytaniu występowała fraza From wykorzystana została właściwość LastIndexOf zmiennej typu String. Inną przydatną ciekawostką jest ustawienia fokusa (kursora) w polu tekstowym w taki sposób, aby jego zawartość nie została zaznaczona. 43
Zadaniem prywatnej funkcji SprawdzZapytanie jest zbadanie, czy w treści zapytania mamy podaną informację z jakiej tabeli będziemy pobierać dane. Jeżeli wszystko jest OK., to funkcja zwraca True, jeżeli nie, to wyświetla stosowny komunikat i zwraca False. 44
Po skomponowaniu zapytania możemy sprawdzić jego funkcjonowanie poprzez klik przycisku opisanego jako Wykonaj (btnwykonaj), co powoduje wykonanie poniższej procedury. Pierwsza instrukcja tej procedury wywołuje funkcję SprawdzZapytanie, jeżeli funkcja zwraca False, to następuje wyjście z procedury. Do właściwości Text etykiety lblwynikzapytania chcemy zwrócić informację o liczbie zwróconych rekordów. Z uwago na język polski korzystamy z pomocniczej funkcji o nazwie OdmienRekordow, której zadaniem jest zwrócenie poprawnej odmiany słowa rekordów. Podobnie jak wcześniej pozostało już tylko pokazanie działania tego formularza. Poniżej dwa przykładowe zrzuty ekranowe. Zaczynamy od wskazania pliku bazy danych, tu jest to plik TestSP.mdb z dysku E:\. 45
Klik przycisku Otwórz wyświetla instancję formularza frmtestsql. Proszę zauważyć, że lista Dostępne tabele zawiera już tabele (użytkownika) utworzone w wybranej bazie danych. 46
Klik jednej z tych tabel powoduje pobranie jej listy pól i umieszczenie ich nazw w liście Pola wybranej tabeli. Jeżeli w liście pól klikniemy np. symbol gwiazdki, to do pola Budowane zapytanie zostanie wstawiony poniższy tekst. Klik przycisku Wykonaj (przy niekompletnym zapytaniu brakuje From Studenci ) może spowodować dwojaką reakcję. Albo zostanie zgłoszony błąd wynikający z konstrukcji zapytania albo funkcja SprawdzZapytanie poprawnie uzupełni zapytanie. 47
Klik przycisku powoduje taki efekt jak poniżej. Wyraźnie widzimy, że funkcja SprawdzZapytanie niezbyt poprawnie skorygowała nasze zapytanie, w rezultacie wystąpił błąd czasu wykonania, skwitowany pokazanym komunikatem. Możemy ręcznie poprawić zapytanie lub zastanowić się, jaki błąd został popełniony w funkcji SprawdzZapytanie. Drugi sposób jest trudniejszy, ale my zajmiemy się poprawieniem funkcjonowania tej funkcji. Co się stało i dlaczego wywołanie tej funkcji zmieniło wyjściowy tekst zapytania Select * na Select From Studenci zamiast Select * From Studenci? Przyczyna jest stosunkowo prosta. W procedurze obsługującej klik pozycji w liście lstpola jest fragment wskazany strzałką. 48
Jeżeli użytkownik wybrał symbol gwiazdki, to do pola txtsql został wstawiony tekst Select * (po gwiazdce jest jeszcze spacja). Z kolei w oryginalnej funkcji SprawdzZapytanie jest taki fragment: Jeżeli w treści zapytania nie było frazy From (a w naszym przykładzie nie było), to treść zapytania została skrócona o dwa znaki w naszym przypadku została skasowana *! Dotychczasowy fragment był dobrze pomyślany w tych sytuacjach, gdy użytkownik wybrał nie gwiazdkę, ale konkretne pole. Wtedy do pola txtsql wstawiany jest tekst Select nazwa_pola, (po nazwie pola dodawany jest przecinek i spacja). W takich sytuacjach poprawienie zapytania polegające na usunięciu dwóch ostatnich znaków i dodanie frazy From jest poprawne! Rozwiązanie jest banalnie proste, wystarczy w przypadku wybrania gwiazdki dodać do instrukcji select nie gwiazdkę i spację, lecz dwie spacje!. Po tej korekcie wszystko jest OK. 49
1.14.2 Zbudowanie dynamicznego formularza Korzystając z obiektu ADOX możemy pobrać informacje o strukturze bazy MS Access (o jej tabelach i polach tych tabel), co daje nam możliwość zbudowania formularza z formantem typu TabControl, gdzie w poszczególnych zakładkach umieścimy zestaw etykiet i pól tekstowych wyświetlających pojedyncze rekordy dla każdej z tabel Projekt formularza frmpojedynczyrekord jest stosunkowo prosty, umieszczono w nim wspomniany wyżej formant TabControl nadając mu nazwę tbczakladki. Wysokość formantu tbczakladki została tak dobrana, aby na dole formularza pozostał wolny pasek, w którym umieszczono cztery przyciski do obsługi nawigacji po rekordach oraz pole tekstowe do wyświetlenia numeru bieżącego rekordu. W prezentowanym rozwiązaniu wysokość formularza została ustawiona na 300 pkt, szerokość na 496 pkt. Z kolei wysokość formantu tbczakladki ustawiono na 227 pkt przy szerokości 475 pkt. Formant ten został ustawiony na pozycji 2, 2 względem lewego górnego narożnika formularza. Przyciski btnpierwszy, btnnastepny, btnpoprzedni oraz btnostatni będą funkcjonować jako wspólne przyciski nawigacyjne pozwalając na przemieszczanie kursora po zestawie rekordów danej tabeli. Między nimi umieszczono pole txtnrrekordu, będzie w nim wyświetlany numer bieżącego rekordu dla bieżącej zakładki (dokładniej: dla tabeli prezentowanej na bieżącej zakładki). Poniżej pełny kod klasy formularza frmpojedynczyrekord, prezentowane są wszystkie procedury i funkcje zabezpieczające funkcjonowanie tego formularza. 50
Imports System Imports System.Data ' import przestrzeni nazw niezbędnej do współpracy z bazą MS Access Imports System.Data.OleDb Public Class frmpojedynczyrekord Private nrzakladki As Integer Private mydataset As New DataSet Private myadapter As OleDbDataAdapter ' tablica X() będzie przechowywać numery bieżących ' rekordów dla każdego z formularzy (tabeli) Private X() As Long Private tabela As String Private Sub frmpojedynczyrekord_load(byval sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load Dim adoconn As New ADODB.Connection, i As Integer = 0 ' deklaracja i utworzenie obiektu ADOX.Catalog Dim mycat As New ADOX.Catalog ' deklaracja zmiennej tbl typu tabela obiektu ADOX na ' potrzeby pętli For Each Dim tbl As ADOX.Table, col As ADOX.Column ' otwieramy połączenie z bazą danych, wywołujemy metodę ' Open obiektu adoconn adoconn.open(strconndynamic) ' wskazujemy aktywne połączenie dla obiektu mycat mycat.activeconnection = adoconn ' deklaracja formantów Dim mylbl As Label Dim mytxt As TextBox Dim j, nrz As Integer, yl As Integer = 10 Dim yt As Integer = 10 ' deklaracja zakładki formantu TabControl Dim mytab As TabPage nrz = -1 For Each tbl In mycat.tables ' jeżeli nie jest to tabela systemowa MS Access If tbl.name.substring(0, 2) <> "MS" Then nrz += 1 ' tworzymy egzemplarz nowej zakładki mytab = New TabPage ' komponujemy jej nazwę mytab.name = "tab" & tbl.name & "_" & nrz.tostring ' definuyjemy jej tytuł mytab.text = tbl.name 51
' dodajemy utworzoną zakładkę do formantu TabControl Me.tbcZakladki.Controls.Add(myTab) ' a teraz dodajemy pola tekstowe i etykiety pól dla danej tabeli j = 0 For Each col In tbl.columns ' tworzymy egzemplarz etykiety mylbl = New Label ' właściwości Text przypisujemy nazwę kolumny mylbl.text = col.name ' tworzymy nazwę etykiety mylbl.name = "lbl" & UsunSpacje(col.Name) ' definiujemy wyrównanie tekstu w etykiecie mylbl.textalign = ContentAlignment.MiddleRight ' ustawiamy właściowość Location ' pierwszy argument jest stały i określa odsunięcie ' etykiety w poziomie od lewej krawędzi formantu ' drugi argument określa położenie pionowe i jest ' dynamicznie zmieniany mylbl.location = New System.Drawing.Point(15, j * 30 + 13) ' ustawiamy także rozmiar etykiety (stały, bo nie wiadomo ' jaki będzie potrzebny) mylbl.size = New System.Drawing.Size(80, 13) ' tworzymy nowy egzemplarz formantu TextBox mytxt = New TextBox ' dynamicznie ustalamy jego położenie mytxt.location = _ New System.Drawing.Point(100, j * 30 + 10) ' określamy rozmiar (szerokość, wysokość) tego formantu mytxt.size = New System.Drawing.Size(330, 20) ' nadajemy mu nazwę skomponowaną z prefiksu 'txt' i nazwy ' kolumny mytxt.name = "txt" & col.name ' dodajemy oba formanty do kolekcji formantów utworzonej ' zakładki mytab.controls.add(mylbl) mytab.controls.add(mytxt) ' powiększamy zmienną j o jeden (liczba par pól ' tekstowych i etykiet j += 1 Next ' wywołujemy prywatną procedurę, jej zadaniem jest pobranie ' danych z bazy i przypięcie ich do odpowiednich pól ' tekstowych 52
PobierzDane(nrZ) End If ' pobranie danych z tabeli zakończone, uruchamiamy kolejną ' Pętlę (obrót) Next ' sprzątamy po sobie adoconn.close() adoconn = Nothing ' jeżeli w bazie była co najmniej jedna kolumna, to If nrz > -1 Then ' zmieniamy rozmiar tablicy X() ReDim X(nrZ) ' w pętli ustawiamy zero na każdej pozycji tej tablicy For i = 0 To nrz X(i) = 0 ' ustawienie zerowego rekordu dla każej zakładki Next ' zakładką bieżącą będzie ta o indeksie 0 nrzakladki = 0 Me.tbcZakladki.TabPages.Item(nrZakladki).Select() ' wywołujemy procedurę wyświetlającą numer aktualnego rekordu ' dla bieżącej zakładki KtoryRekord() Else MsgBox("Nie odczytano żadnej tabeli użytkownika") End If End Sub Private Function UsunSpacje(ByVal nazwa As String) As String ' jeżeli nazwa pola zawiera spacje, to będą zastąpione podkreśleniem Return nazwa.replace(" ", "_") End Function Private Sub PobierzDane(ByVal nrz As Integer) ' ustawiamy zakładkę o indeksie nrz jako aktywną Me.tbcZakladki.TabPages(nrZ).Select() Dim txtsql As String, i As Integer tabela = tbczakladki.tabpages(nrz).text ' zbudowanie dynamicznego zapytania select * from nazwa_tabeli txtsql = "select * from " & tabela ' deklarujemy i tworzymy instancję obiektu Connection Dim jg As New OleDbConnection(strConnDynamic) ' kolejne instrukcje w bloku Try - Catch Try jg.open() ' otwarcie połączenia 53
' zbudowanie egzemplarza obiektu Adapter w oparciu ' o zdefiniowane zapytanie i otwarte połączenie myadapter = New OleDbDataAdapter(txtSql, jg) ' wywołanie metody Fill z poleceniem przeniesienia do ' obiektu mydataset pobranych rekordów, zaczynając od rekordu ' o numerze 0, z określeniem maksymalnej liczby rekordów, ' pobrane dane mają utworzyć obiekt DataSet o nazwie tabela ' do zmiennej 'i' metoda Fill zwraca liczbę pobranych rekordów i = myadapter.fill(mydataset, 0, MaxLiczbaRekordow, tabela) ' jeżeli pobrano jakieś rekordy,... If i > 0 Then ' deklaracja obiektu ctl na potrzeby pętli For Each Dim ctl As Control ' przeglądamy kolekcję kontrolek wskazanej zakładki For Each ctl In tbczakladki.tabpages(nrz).controls ' jeżeli aktualny formant jest polem tekstowym, to... If TypeOf (ctl) Is TextBox Then ' wywołujemy metodę DataBindings tego formantu ' nakazując przypięcie do właściwości Text danych, ' Które będą pobierane z obiektu mydataset z obiektu ' typu DataTable o nazwie 'tabela' z kolumny, której ' nazwa jest zgodna z nazwą formantu ctl po odrzuceniu ' prefiksu txt ctl.databindings.add("text", mydataset, _ tabela & "." & ctl.name.substring(3, _ ctl.name.length - 3)) End If Next End If Catch ex As Exception MsgBox("Problem z pobraniem lub przypięciem danych do pól " & _ "tekstowych", MsgBoxStyle.Critical, "Dynamiczne formularze") Finally jg.close()' sprzątamy po sobie jg = Nothing End Try End Sub Private Sub btnpoprzedni_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnpoprzedni.click ' Dim tabela As String = tbczakladki.selectedtab.text Me.BindingContext(myDataSet, tabela).position -= 1 KtoryRekord() End Sub 54
Private Sub tbczakladki_click(byval sender As Object, _ ByVal e As System.EventArgs) Handles tbczakladki.click tabela = tbczakladki.selectedtab.text nrzakladki = Me.tbcZakladki.SelectedIndex Me.BindingContext(myDataSet, tabela).position = X(nrZakladki) KtoryRekord() End Sub Private Sub btnnastepny_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnnastepny.click Dim tabela As String = tbczakladki.selectedtab.text Me.BindingContext(myDataSet, tabela).position += 1 KtoryRekord() End Sub Private Sub btnpierwszy_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnpierwszy.click Dim tabela As String = tbczakladki.selectedtab.text Me.BindingContext(myDataSet, tabela).position = 0 KtoryRekord() End Sub Private Sub btnostatni_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnostatni.click Dim tabela As String = tbczakladki.selectedtab.text Me.BindingContext(myDataSet, tabela).position = _ mydataset.tables(tabela).rows.count KtoryRekord() End Sub Private Sub KtoryRekord() Dim pozycja As Long = _ Me.BindingContext(myDataSet, tabela).position X(nrZakladki) = pozycja Me.txtNrRekordu.Text = "Rekord nr " & (pozycja + 1).ToString If pozycja + 1 = mydataset.tables(tabela).rows.count Then Me.btnOstatni.Enabled = False Me.btnNastepny.Enabled = False Else Me.btnOstatni.Enabled = True Me.btnNastepny.Enabled = True End If If pozycja = 0 Then Me.btnPierwszy.Enabled = False Me.btnPoprzedni.Enabled = False 55
Else Me.btnPierwszy.Enabled = True Me.btnPoprzedni.Enabled = True End If End Sub End Class Instancja formularza frmpojedynczyrekord jest uruchamiana tylko wtedy, gdy wcześnie wykonane zostało polecenie Pobranie tabel i pól, test zapytania z menu ADO (COM), ponieważ wtedy do zmiennej globalnej strconndynamic zostanie przypisana ścieżka do pliku bazy danych MS Access. Poniżej stosowny fragment kodu z klasy formularza głównego naszej aplikacji (czyli frmmdiform). Private Sub mnudynamiczneformularze_click(byval sender As _ System.Object, ByVal e As System.EventArgs) _ Handles mnudynamiczneformularze.click If strconndynamic.length > 0 Then Dim frm As New frmpojedynczyrekord frm.mdiparent = Me frm.show() Else MsgBox("Polecenie niedostępne, baza danych nie jest znana") End If End Sub Poniżej widok zbudowanego formularza w tracie pracy, pokazany jest formularz ekranowy tabeli Pracownicy. 56
1.15 Opis pozycji SQL Ten fragment aplikacji wykorzystuje przykładową tabelę o nazwie Klienci, której definicję przedstawiono w rozdziale 2.1 tej pozycji. Kolejno zostanie przedstawione wykorzystanie obiektu Adapter do przeglądania i modyfikowania rekordów tej tabeli oraz bardziej zaawansowane przetwarzanie informacji z tej tabeli z pomocą procedur przechowywanych. 1.15.1 Adapter Formularz frmadaptersqlcmdb zawiera dwa formanty: DataGridView o nazwie dgvdane, w którym będziemy prezentować rekordy tabeli Klienci oraz przycisk polecenia (Button) o nazwie btnupdate. Przyciski minimalizacji i maksymalizacji formularza zostały pozostawione po to, aby użytkownik mógł modyfikować rozmiar tego formularza, jeżeli będzie zachodziła taka potrzeba. Z tych samych powodów pozostawiono możliwość ręcznego zmodyfikowania rozmiarów tego formularza (właściwość FormBorderStyle ustawiona na Sizable). Z tych powodów (dopuszczenia modyfikacji rozmiarów formularza) zmodyfikowano właściwość Anchor obu formantów. W przypadku dgvdane została ona ustawiona na zakotwiczenie tego formantu wg wszystkich czterech krawędzi formularza. 57
W przypadku przycisku btnupdate właściwość Anchor ustawiono wg prawej i dolnej krawędzi formularza. W kodzie klasy formularza zadeklarowano potrzebne zmienne oraz utworzono dwie procedury odpowiedzialne za jego funkcjonowanie. Poniżej kod tych procedur wraz z obszernymi komentarzami. ' Import potrzebnych przestrzeni nazw Imports System.Data Imports System.Data.SqlClient Public Class frmsqlupdateklienci ' deklaracja zmiennych modułu dla klasy formularza ' ich deklaracja w tym miejscu powoduje, że będą dostępne ' dla wszystkich procedur i funkcji w tej klasie Dim sqladapter As SqlDataAdapter Dim ds As DataSet ' tablica xupdate ma pełnić rolę wskaźnika czy jest potrzebna ' aktualizacja Dim xupdate() As Boolean Private Sub frmsqladaptercmdb_load(byval sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ' dekalracja i utworzenie obiektu SQLConnection ' zmienna globalna strconnsql zawiera łańcuch połączenia do ' bazy SQL Dim conn As New SqlConnection(strconnSqL) ' utworzenie nowego egzemplarza obiektu DataSet ds = New DataSet ' dalsze instrukcje mogą spowodować błąd, stąd są w bloku ' Try-Catch Try ' otwieramy połaczenie z bazą conn.open() ' tworzymy nowy obiekt SqlDataAdapter przekazując do niego ' polecenie SQL i otwarte połączenie. Efektem jest dostarczenie ' do niego danych zgodnie z zapytaniem 58
sqladapter = New SqlDataAdapter("select * from dbo.klienci", _ conn) ' utworzenie egzemplarza obiektu SqlCommandBuilder przekazując ' mu obiekt Adapter ' rezultatem jest utworzenie 3 poleceń wykonujących Insert, ' Update i Delete Dim cmdbuilder As New SqlCommandBuilder(sqlAdapter) ' wywołanie metody Fill nakazujacej wypełnienie pobranymi ' danymi obiektu ds (DataSet) sqladapter.fill(ds) ' określamy żródło danych obiektu DataGridView Me.dgvDane.DataSource = ds.tables(0) ' zerowa kolumnna ma być niewidoczna Me.dgvDane.Columns(0).Visible = False Catch ex As Exception ' wystąpił błąd, zwracamy komunikat MsgBox("Problem z dostępem do bazy damych") Finally ' ten fragment jest wykonywany zawsze ' zamykamy połączenie conn.close() ' i zwalniamy zasób conn = Nothing End Try End Sub Private Sub btnupdate_click(byval sender As Object, _ ByVal e As System.EventArgs) Handles btnupdate.click Dim conn As New SqlConnection(strconnSqL) ' deklaracja i utworzenie nowego obiektu typu DataTable Dim dtzmiany As New DataTable Try conn.open() ' do obiektu DataTable pobieramy zmiany z tabeli o ' indeksie 0 z DataSet dtzmiany = ds.tables(0).getchanges ' jeżeli były zmienione rekordy to... If dtzmiany IsNot Nothing Then ' wywołujemy metodę Update obiektu Adapter ' metoda aktualizuje dane w bazie danych wykorzystując ' obiekt dtzmiany sqladapter.update(dtzmiany) ' komunikat na ekran MsgBox("AKtualizacja tabeli w bazie SQL zrobiona") 59
End If Catch ex As Exception ' ewentualny komunikat o błędzie MsgBox(ex.ToString) Finally 'sprzątamy conn.close() conn = Nothing End Try End Sub End Class Poniżej widok tego formularza w pracy. Pierwszy zrzut ekranowy pokazuje ten formularz w takich rozmiarach, jak został zaprojektowany. Efekt jest taki, że w formancie dgvdane został uaktywniony pasek poziomego przewijania, ponieważ część informacji (kolumn) pobranych z bazy danych nie jest widoczna. Możemy za pomocą myszy zmodyfikować rozmiar tego formularza tak, aby wyświetlał komplet kolumn (zniknie wtedy pasek poziomego przewijania). Dzięki ustawieniu właściwości Anchor dla formantu dgvdane tak, aby były zachowane jego odległości od wszystkich krawędzi formularza oraz ustawieniu tej samej właściwości dla przycisku btnupdate na trzymanie odległości względem prawej i dolnej krawędzi formularza jego wygląd po zmianie rozmiaru jest dokładnie taki, jakiego oczekujemy. 60
A co by było, gdybyśmy nie zmienili właściwości Anchor dla obu formantów? Konsekwencje pokazane są poniżej. Bez problemy zmieniamy rozmiar formularza, ale rozmiary i położenie obu formantów pozostały bez zmian. W przypadku dgvdane nic nam nie dało powiększenie rozmiarów formularza, ponieważ nie nastąpiło powieszenie rozmiarów tego formantu (aby zachować stałe, nadane na etapie projektowania, odległości od krawędzi formularza). Z kolei przycisk btnupdate pozostał na swoim miejscu, efekt jest taki, że albo będzie dla nas niedostępny, albo będzie nam przeszkadzał (wtedy, gdybyśmy ustawili poprawnie właściwość Anchor dla dgvdane). 61
1.15.2 Update tabeli via procedura przechowywana (klasycznie) Kolejna pozycja w menu SQL dotyczy wykonania aktualizacji danych w bazie za pośrednictwem procedury przechowywanej SQL. Działanie takie jest zdecydowanie lepsze od wykorzystania możliwości, jakie daje obiekt CommandBuilder, zwłaszcza w sytuacjach, gdy aktualizacja dotyczy tabeli powiązanej relacjami z innymi tabelami. Dla zademonstrowania takiego podejścia potrzebna nam będzie procedura przechowywana SQL realizująca zadanie aktualizacji wskazanego rekordu danej tabeli. Kod takiej procedury o nazwie pupdateklienci został zaprezentowany w rozdziale 2.1. Procedura pupdateklienci jest procedurą z parametrami, dla jej wykonania musimy przekazać do obiektu Command w modelu ADO.NET nie tylko nazwę procedury, lecz także szereg właściwości charakteryzujących poszczególne parametry. Będą to takie informacje jak nazwa parametru, jego typ, rozmiar, wartość oraz kierunek (może być parametr typu input lub output). Parametry te będą tworzyć kolekcję parametrów, która będzie elementem obiektu Command. Wykorzystamy w sensie projektu analogiczny formularz jak w poprzednim rozdziale, tym razem jego nazwa to frmsqlupdateklienci. Umieszczono na nim formant typu DataGridView o nazwie dgvdane oraz przycisk polecenie o nazwie btnupdate. Oba formanty mają, podobnie jak w formularzu frmadaptersqlcmdb, zmodyfikowaną właściwość Anchor w celu umożliwienia swobodnej zmiany rozmiaru formularza. Zasadnicza różnica kryje się w kodzie klasy tego formularza. Poniżej wszystkie instrukcje tej klasy wraz ze stosownymi komentarzami. Imports System.Data Imports System.Data.SqlClient Public Class frmsqlupdateklienci ' delaracja potrzebnych stałych i zmiennych Const txtkom As String = "Klasyczne wywołanie procedury SQL" Dim sqladapter As SqlDataAdapter Dim ds As DataSet ' tablica ta będzie wykorzystana jako wskaźnikowa ' w tym sensie, czy mamy robić update czy nie danego rekordu Dim xupdate() As Boolean Private Sub frmsqladaptercmdb_load(byval sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ' deklarujemy i tworzymy instancję obiektu connection Dim conn As New SqlConnection(strconnSqL) ' deklarujemy i tworzymy instację obiektu Command 62
Dim cmd As New SqlCommand ' deklarujemy i tworzymy instancję obiektu dataset Dim ds As New DataSet Dim i As Integer ' reszta działań może spowodować błądm stąd użycie bloku ' Tray-Catch Try conn.open() ' przypisanie do właściowości Connection obiektu cmd ' otwartego połączenia cmd.connection = conn ' określenie typu polecenia cmd.commandtype = CommandType.StoredProcedure ' podanie nazwy procedury przechowywanej cmd.commandtext = "dbo.pdajdaneklientow" ' przygotowania do odebrania danych, które zwróci procedura ' deklarujemy obiekt typu DataReader Dim dr As SqlDataReader ' poprzez wywołanie metody ExecuteReader dostarczamy dane ' do DataReader dr = cmd.executereader ' deklarujemy i tworzymy obiekt typu Dataset Dim dt As New DataTable ' wywołujemy metodę Load utworzonego obiektu wskazując jako ' źródło danych obiekt dr (DataReader) dt.load(dr) ' tworzymy źródło danych dla obiektu DataGridView Me.dgvDane.DataSource = dt ' ukrywamy zerową kolumnę Me.dgvDane.Columns(0).Visible = False ' deklarujemy wymiary tablicy ReDim xupdate(dt.rows.count - 1) ' na wszystkich pozycjach tablicy ustawiamy wartość False For i = 0 To UBound(xUpdate) xupdate(i) = False Next Catch ex As Exception MsgBox("Problem z wykonaniem procedury pdajdaneklientow") Finally conn.close() cmd = Nothing conn = Nothing End Try ' ustawienie zmiennej globalnej flaga na True 63
flaga = True End Sub Private Sub btnupdate_click(byval sender As Object, _ ByVal e As System.EventArgs) Handles btnupdate.click Dim conn As New SqlConnection(strconnSqL), i, j As Integer Try conn.open() ' deklaracja i utworzenie egzemplarza (instancji) ' obiektu Command Dim cmd As New SqlCommand ' przypisanie mu potrzebnych informacji cmd.connection = conn cmd.commandtype = CommandType.StoredProcedure cmd.commandtext = "dbo.pupdateklienci" ' zerujemy kolekcję parametrów cmd.parameters.clear() ' deklarujemy nowy parametr Dim param As SqlParameter ' w pętli po wszystkich rekordach badamy, czy jest potrzebna ' aktualizacja For i = 0 To UBound(xUpdate) If xupdate(i) Then ' jest potrzebna, a więc... ' tworzymy kolekcję parametrów ' obiekt param staje się nowym parametrem o określonej ' nazwie. typie danych i rozmiarze ' rozmiar (ostatni parametr) jest niezerowy dla zmiennych typu String param = New SqlParameter("@idk", SqlDbType.Int, 0) ' do właściwości Value przypisujemy wartość parametru ' pobraną z odpowiedniej kolumny i wiersza obiektu typu ' DataGridView. Musimy pamiętać o tym, ' że rekordy i kolumny są indeksowane od zera param.value = Me.dgvDane.Rows(i).Cells(0).Value ' określamy, czy parametr jest wchodzący czy wychodzący param.direction = ParameterDirection.Input ' dodajemy utworzony parametr do kolekcji parametrów cmd.parameters.add(param) ' param = New SqlParameter("@nazwa", _ SqlDbType.NVarChar, 100) param.value = Me.dgvDane.Rows(i).Cells(1).Value param.direction = ParameterDirection.Input cmd.parameters.add(param) 64
' param = New SqlParameter("@adres", _ SqlDbType.NVarChar, 100) param.value = Me.dgvDane.Rows(i).Cells(2).Value param.direction = ParameterDirection.Input cmd.parameters.add(param) ' param = New SqlParameter("@nip", SqlDbType.NVarChar, 13) param.value = Me.dgvDane.Rows(i).Cells(3).Value param.direction = ParameterDirection.Input cmd.parameters.add(param) ' param = New SqlParameter("@mail", SqlDbType.NVarChar, 50) param.value = Me.dgvDane.Rows(i).Cells(4).Value param.direction = ParameterDirection.Input cmd.parameters.add(param) ' param = New SqlParameter("@telefon", _ SqlDbType.NVarChar, 50) param.value = Me.dgvDane.Rows(i).Cells(5).Value param.direction = ParameterDirection.Input cmd.parameters.add(param) ' param = New SqlParameter("@osoba", _ SqlDbType.NVarChar, 100) param.value = Me.dgvDane.Rows(i).Cells(6).Value param.direction = ParameterDirection.Input cmd.parameters.add(param) ' wszystkie parametry dla danego wiersza są już określone ' wywołujemy więc polecenie (metodę) ExewcuteNonQuery ' która odpowiedzialna jest za wykonanie polecenia ' modyfikującego cmd.executenonquery() End If Next ' wszystkie rekordy wymagające aktualizacji zostały Zmodyfikowane, sprzątamy po sobie param = Nothing cmd = Nothing MsgBox("Aktualizacja zakończona", MsgBoxStyle.Information, _ txtkom) Catch ex As Exception ' komunikat o błędzie 65
MsgBox("problem z wykonaiem procedury przechowywanej", _ MsgBoxStyle.Critical, txtkom) Finally ' zamykamy i zwalniamy obiekt Connection conn.close() conn = Nothing End Try End Sub Private Sub dgvdane_cellendedit(byval sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles dgvdane.cellendedit ' procedura wywoływana jest w momencie zakończenia edycji dowolnej ' komórki w danym wierszu DataGridView ' jeżeli flaga nie jest True to wyjdź If Not flaga Then Exit Sub ' konieczna jest zmiana w tabeli xupdate na pozycji, która jest ' Określona właściwością RowIndex argumentu e przekazanego do tej ' procedury z obiektu DataGridView. Inną właściwością argumentu e ' jest ColumnIndex, w sumie obie właściwości dają nam ' pełną informację o komórce, która została zaktualizowana ' poniższa instrukcja najpierw sprawdza, czy ten wiersz już nie ' został oznaczony jako wymagający aktualizacji, jeżeli nie, to na ' pozycji e.rowindex wpisujemy True If Not xupdate(e.rowindex) Then xupdate(e.rowindex) = True End Sub End Class Poniżej widok tego formularza w trakcie pracy. Po jego otwarciu został zmodyfikowany jego rozmiar tak, aby pola (kolumny) Adres oraz OsobaKontaktowa były w pełni widoczne. W ostatniej kolumnie pierwszego wiersza dopiszemy imiona do istniejącego nazwiska Abacki. 66
Dane zostały uzupełnione, symbol ołówka na początku gridu identyfikuje aktualnie modyfikowany rekord (wiersz). Klik przycisku Aktualizuj uruchamia procedurę aktualizującą. Po jej zakończeniu wyświetlany jest stosowny komunikat. Dla pokazania, że faktycznie zmiany w tabeli Klienci zostały utrwalone formularz został zamknięty i ponownie otwarty, jak widzimy pierwszy rekord został zmodyfikowany. Z przedstawionego kodu wynika, że taka standardowa (klasyczna) metoda wywołania procedury przechowywanej, zwłaszcza z dużą liczbą parametrów, jest dość uciążliwa. W kolejnym podrozdziale pokazana zostanie rozwiązanie, które znacznie ułatwia tworzenie takiego kodu (obsługującego wywołanie procedur przechowywanych SQL). 67
1.15.3 Update tabeli via procedura przechowywana wg JG W tym przykładzie wykorzystamy formularz o nazwie frmsqlupdateklienci2, ma on dokładnie taki sam zestaw formantów jak swój poprzednik, ale całkowicie inny kod. Wynika on między innymi z tego, że zaprojektujemy klasę, z pomocą której będziemy obsługiwać funkcjonowanie tego formularza. Klasa CTestSql Jest to klasa potomna (dziedzicząca) po klasie bazowej CForStorageSub, której kod będzie omówiony w kolejnym rozdziale. W klasie na potrzeby tego przykładu utworzono jeden interfejs z trzema metodami. Pełny kod wraz z komentarzami pokazany jest niżej. Imports System.Data Imports System.Data.SqlClient ' dekalrujemy interfejs klasy Public Interface IKlienci ' ten interfejs będzie obsługiwał operacje na tabeli Klienci ' deklarujemy funkcję, która będzie zwracać komunikat o ' ewentualnym błędzie Function Komunikat() As String ' dekalrujemy metodę odpowiedzialną za pobranie danych z ' tabeli Klienci Sub PrzygotujDaneDoEdycji(ByVal strconn As String, _ ByVal frm As frmsqlupdateklienci2) ' deklarujemy metodę odpowiedzialną za wykonanie aktualizacji ' danychx Sub WykonajUpdate(ByVal strconn As String, ByVal Xb() As Boolean, _ ByVal dgv As DataGridView) End Interface Public Class CTestSql ' tworzona klasa dziedziczy po klasie bazowej CForStorageSub Inherits CForStorageSub ' kod będzie implementował interfejs IKlienci Implements IKlienci ' deklarujemy prywatną zmienną mkomunikat i inicjalizujemy ją pustym ' ciągiem znaków Private mkomunikat As String = "" Public Function Komunikat() As String Implements IKlienci.Komunikat ' funkcja (metoda) zwraca zmienną prywatną mkomunikat Return mkomunikat End Function 68
Public Sub PrzygotujDaneDoEdycji(ByVal strconn As String, _ ByVal frm As frmsqlupdateklienci2) _ Implements IKlienci.PrzygotujDaneDoEdycji ' do metody przekazujemy jako parametry (argumenty) łańcuch ' połączenia oraz formularz będący instancją formularza ' frmsqlupdateklienci2 ' ' deklarujemy i tworzymy instancję obiektu Connection Dim conn As New SqlConnection(strconn) ' dekalrujemy obiekt typu DataTable Dim dt As DataTable ' reszta instrukcji w bloku obsługi ewentualnego błędu Try ' otwracie połączenia z bazą SQL conn.open() ' korzystamy z metody klasy bazowej i wywołujemy funkcję ' DajRekordset. Lista publicznych zmiennych, metod i ' właściwości klasy bazowej jest dostępna po słowie kluczowym ' MyBase i kropce. Wszystkie metody klasy bazowej, które ' wykorzystują pojedynczą procedurę przechowywaną mają podobny ' zestaw argumentów. Jest to składnia: ' Nazwa_Metody(obiekt_Connection, nazwa_procedury, ' lista_parametrow) ' jeżeli procedura nie ma parametrów, to ostatni składnik jest ' słowem "brak" ' w tym przypadku przekazujemy obiekt conn, nazwę procedury ' wraz z jej prefiksem oraz wyraz "brak" jako sygnał, że ' procedura nie ma argumentów. ' Metoda DajRekordset zwraca obiekt typu DataTabel, który ' przypisujemy do zmiennej dt dt = MyBase.DajRekordset(conn, "dbo.pdajdaneklientow", "brak") ' definiujemy teraz żródło danych dla formantu dgvdane frm.dgvdane.datasource = dt ' ukrywamy kolumnę zerową z kluczem tabeli frm.dgvdane.columns(0).visible = False Catch ex As Exception mkomunikat = "Problem z wykonaniem procedury pdajdaneklientow" Finally ' sprzątamy po sobie conn.close() conn = Nothing End Try End Sub 69
Public Sub WykonajUpdate(ByVal strconn As String, ByVal Xb() As _ Boolean, ByVal dgv As DataGridView) _ Implements IKlienci.WykonajUpdate ' ta metoda odpowiada za aktualizację tych rekordów gridu dgv, ' które wymagają aktualizacji. Informacja o tym, które rekordy ' zawarte jest w tablicy Xb Dim conn As New SqlConnection(strconn) Try conn.open() ' wywołujemy metodę UpdateWieleRekordow z klasy bazowej ' przekazujemy do niej argumenty: otwarte połączenie, nazwę ' procedury SQL, obiekt typu DataGridView jako źródło rekordów ' do aktualizacji, tablicę logiczną oraz zestaw nazw parametrów ' zawierających dla każdego z nich następujące informacje: ' nazwa parametru, jego typ, rozmiar oraz numer kolumny gridu, ' z której ma być pobrana jego wartość MyBase.UpdateWieleRekordow(conn, "dbo.pupdateklienci", _ dgv, Xb, _ "@idk", jgtyp.jginteger, 0, 0, _ "@nazwa", jgtyp.jgstring, 100, 1, _ "@adres", jgtyp.jgstring, 100, 2, _ "@nip", jgtyp.jgstring, 13, 3, _ "@mail", jgtyp.jgstring, 50, 4, _ "@telefon", jgtyp.jgstring, 50, 5, _ "@osoba", jgtyp.jgstring, 100, 6) Catch ex As Exception mkomunikat = "Problem z update wielu rekordów" Finally conn.close() conn = Nothing End Try End Sub End Class Kod formularza frmsqlupdateklienci2 Fakt utworzenia klasy CTestSql znakomicie skraca i upraszcza kod, który musimy utworzyć w klasie tego formularza dla zabezpieczenia jego poprawnego funkcjonowania. Public Class frmsqlupdateklienci2 'deklaracja stałej i tablicy Const txtkom As String = "Wywołanie procedury SQL wg JG" Dim xupdate() As Boolean 70
Private Sub frmsqlupdateklienci2_load(byval sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load 'przedefiniowanie rozmiarów tablicy xupdate ReDim xupdate(me.dgvdane.rows.count - 1) ' ustawienie na każdej składowej wartości False For i As Integer = 0 To Me.dgvDane.Rows.Count - 1 xupdate(i) = False Next ' przypisanie wartości True do zmiennej globalnej flaga ' umożliwi rejestrowanie ewentualnych aktualizacji w gridzie flaga = True End Sub Private Sub btnupdate_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles btnupdate.click ' deklarujemy obiekt klasy CTestSql wykorzystując interfejs ' IKlienci Dim w As IKlienci ' tworzymy instancję klasy CTestSql z ogranieczeniem dostępu do ' metod udostępnionych przez interfejs IKlienci w = New CTestSql ' wywołujemy metodę WykonajUpdate przekazując do niej potrzebne ' argumenty, czyli łańcuch połączenia, tablicę xupdate oraz grid ' dgvdane w.wykonajupdate(strconnsql, xupdate, Me.dgvDane) ' sprawdzamy, czy wszystko przebiegło poprawnie If w.komunikat.length > 0 Then ' niestety nie, komunikat na ekran, jego treść została ' przygotowana w klasie CTestSql MsgBox(w.Komunikat, MsgBoxStyle.Critical, txtkom) Else MsgBox("Aktualizacja rekordów zakończona", _ MsgBoxStyle.Information, txtkom) End If End Sub Private Sub dgvdane_cellendedit(byval sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles dgvdane.cellendedit If Not flaga Then Exit Sub ' jeżeli jeszcze nie odnotowano, że wiersz (rekord) o indeksie ' e.rowindex jest aktualizowany, to ustaw na jego pozycji True If Not xupdate(e.rowindex) Then xupdate(e.rowindex) = True End Sub End Class 71
Kod uruchamiający formularz Formualarz frmsqlupdateklienci2 będzie uruchamiany poprzez klik odpowiedniego polecenia w menu SQL tej aplikacji. Musimy zadbać o to, aby formularz był pokazany dopiero wtedy, gdy pomyślnie pobrano dane z bazy danych. Jeżeli nie, to formularz nie jest pokazywany, a w jego miejsce wyświetlamy stosowny komunikat wyjaśniający sytuację. Poniżej kod procedury obsługującej klik polecenia mnuupdatejg z formularza frmmdiform. Private Sub mnuupdatejg_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles mnuupdatejg.click Dim frm As New frmsqlupdateklienci2 flaga = False ' dekalracja obiektu klasy CTestSql ' z wykorzystaniem interfejsu IKlienci Dim w As IKlienci ' utworzenie instancji klasy CTestSql ' z uwagi na sposób deklaracji dostępne będa ' metody zadekalrowane w interfejsie IKlienci w = New CTestSql ' wywołanie metody PrzygotujDaneDoEdycji w.przygotujdanedoedycji(strconnsql, frm) ' sprawdzenie, czy pomyślnie pobrano dane z bazy ' i czy zostały przypisane do gridu formularza If w.komunikat.length = 0 Then ' tak, pokazujemy formularz ' ale obiekt w nie jest już potrzebny, zwalniamy zasób w = Nothing frm.mdiparent = Me frm.show() Else ' nie, komunikat na ekran MsgBox(w.Komunikat, MsgBoxStyle.Critical, _ "Błąd przygotowania danych") End If End Sub Klika zrzutów pokazujących pracę tego formularza Na zakończenie klika zrzutów pokazujących formularz w trakcie edycji danych zapisanych w tabeli Klienci. Powiedzmy, że w ostatniej kolumnie drugie imię Kamil dopisano pomyłkowo w pierwszym rekordzie zamiast w drugim. 72
Poprawiamy błąd kasując Kamil w pierwszym rekordzie i dopisując to imię w drugim. Błędy poprawione, wykonujemy klik przycisku Aktualizuj. Procedura aktualizująca wykonuje swoją pracę i wyświetla stosowny komunikat. Na zakończenie zamykamy formularz i ponownie go otwieramy, jak widzimy zmiany są w bazie. 73