środa, 30 stycznia 2013

70-516 Monitorowanie wydajności LINQu, Entity Framework

Używając LINQ2SQL oraz Entity Framework możemy na bieżąco monitorować wysyłane zapytania do bazy danych.
Aby logować zapytania wysyłane przez LINQ2SQL należy na obiekcie DataContext ustawić właściwość Log. Przypisać do niej możemy dowolny obiekt klasy dziedziczącej po TextWriter.
W książce do egzaminu podano bardzo dobry przykład stworzenia klasy dziedziczącej po TextWriter która wypisuje zapytania LINQ do okienka Output. Zaletą klasy jest możliwość włączania logowania za pomocą konfiguracji w App/Web .config Zrealizowane jest to za pomocą klasy BooleanSwitch
Przykład który znajduje się w książce:

Code:
    public class LinqToSqlTraceWriter : TextWriter
    {
        private BooleanSwitch logSwitch;
        public LinqToSqlTraceWriter(string switchName)
        {
            logSwitch = new BooleanSwitch(switchName, switchName);
        }

        public override Encoding Encoding
        {
            get { throw new SystemNotImplementedException(); }
        }

        public override void Write(char value)
        {
            if (logSwitch.Enabled)
            {
                Trace.Write(value);
            }
        }

        public override void Flush()
        {
            Trace.Flush();
        }

        public override void Close()
        {
            Trace.Close();
        }
    }

Przełącznik konfigurujemy w app/web configu:

Code:
  <system.diagnostics>
    <switches>
      <add name="LinqToSqlSwitch" value="1"/>
    </switches>
  </system.diagnostics>

Aby włączyć logowanie należy obiektowi DataContext.Log przypisać obiekt klasy LingToSqlTextWriter.

W przypadku Entity Framework treść zapytania wysyłanego do bazy danych otrzymujemy wywołując na obiekcie klasy ObjectQuery<T> metodę ToTraceString. Sposób w jaki to zrobić znaleźć można w innym moim wpisie: http://patryknet.blogspot.com/search?q=ToTraceString


Innym sposobem śledzenia wydajności jest zaimplementowanie Monitora wydajności:

Code:
            string catName = "LINQ";
            if (!PerformanceCounterCategory.Exists(catName))
            {
                PerformanceCounterCategory.Create(catName, "Category help",
                                                  PerformanceCounterCategoryType.SingleInstance, "LINQ2SQL", "Pomoc");
            }
            var performanceCounter = new System.DiagnosticsPerformanceCounter(catName, "LINQ2SQL", false);
            performanceCounter.IncrementBy(100);

wtorek, 29 stycznia 2013

70-516 Tworzenie WCF Data Services - OData

OData to protokół stworzony przez Microsoft, służący przesyłaniu danych za pomocą protokołu HTTP.
Obsługuje następujące metody:

  • GET - selekcja danych (SELECT)
  • POST - wysyłanie danych (INSERT)
  • PUT - uaktualnianie (UPDATE)
  • DELETE - usuwanei danych
Ważnym aspektem jest udzielanie uprawnień dostępu do danych. Możemy przydzielić prawa zarówno na kolekcje zwracane przez serwis jak i operacje, które udostępnia.
Definicja typu wyliczeniowego odpowiadającego za przydział uprawnień:


Code:
  [Flags]
  public enum EntitySetRights
  {
    None = 0, //brak uprawnień do jakichkolwiek operacji
    ReadSingle = 1, //możliwość pobrania tylko jednego elementu z kolekcji
    ReadMultiple = 2, //Możliwość pobrania całej kolekcji
    AllRead = ReadMultiple | ReadSingle, //Odczyt zarówno pojedynczych rekordów jak i całych kolekcji
    WriteAppend = 4,//pozwolenie na dodawanie
    WriteReplace = 8,//pozwolenie na zmianę
    WriteDelete = 16,//pozwolenie na usuwanie
    WriteMerge = 32,//pozwala łączyć dane
    AllWrite = WriteMerge | WriteDelete | WriteReplace | WriteAppend,//wszystkie możliwości manipulacji danymi
    All = AllWrite | AllRead,//wszystkie operacje dozwolone
  }

Aby stworzyć prosty serwis OData postępujemy według instrukcji:
1. Tworzymy nowy projekt ASP.NET
2. Mapujemy za pomocą ORMa np. Entity Framework bazę danych/plik XML który chcemy udostępnić
3. Dodajemy do projektu nowy element typu WCF Data Service:


4. W moim przypadku użyłem bazy danych AdventureWorks. Za pomocą narzędzia Entity Framework Power Tools wygenerowałem model bazy danych i uzyskałem kontekst AdventureWorksLTContext
5. Dodany serwis WCF ma następującą definicję:


Code:
//------------------------------------------------------------------------------
// <copyright file="WebDataService.svc.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;

namespace ODataService
{
    public class AdventureOData : DataService< T >
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
            // Examples:
            // config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }
    }
}

Aby udostępnić stworzone wersje do odczytu wystarczy w stworzonej klasie zmienić T na typ kontekstu oraz dodać uprawnienia:


Code:
    public class AdventureOData : DataService<AdventureWorksLTContext>
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }
    }

Dodając uprawnienia wprowadziłem gwiazdkę co oznacza iż ustawienie dotyczy wszystkich encji.
Teraz otwieramy przeglądarkę i zmieniamy ustawienie - dla przykładu posłużyłem się przeglądarką IE:


Należy odznaczyć zaznaczoną opcję. Po tej operacji możemy uruchomić naszą aplikację i w pasku przeglądarki wprowadzić adres:



Przykłady zapytań:
Wyciągnięcie klienta o id (klucz główny) = 10 realizujemy zapytaniem: /AdventureOData.svc/Customers(10)
Jeżeli klucz jest łańcuchem znakowym (string) wartość zawieramy w apostrofach, np. Encja('Ala')
Aby pobrać zamówienia klienta o Id 29736 należy użyć właściwości która, prowadzi do encji zamówień:
AdventureOData.svc/Customers(29736)/SalesOrderHeaders
Aby wyświetlić zawartość kolumny imię stosujemy zapytanie: AdventureOData.svc/Customers(29736)/FirstName

Dodatkowe przełączniki wprowadzamy według zapytania: AdventureOData.svc/Customers?$przelacznik
Typy przełączników:

  • $count - zlicza ilość elementów
  • $select=kolumna1,kolumna2 - wyświetla wybrane kolumny
  • $orderby = kolumna1 [desc], kolumna2 [desc] - sortowanie
  • $top=x - wyświetla x rekordów
  • $skip=10&$top=10 - stronicowanie, w takim przypadku warto posortować rekordy, aby dostawać na każdej stronie unikalne wartości
  • $filter - filtrowanie wyniku
  • $expand - wczesne ładowanie (eager loading) - po przecinkach wymieniamy właściwości odpowiadające za nawigację. Jeżeli wymieniona właściwość posiada kolejne referencje do innych encji wymieniamy je po ukośniku "/"
  • $metadata / ?wsdl - schema serwisu
Aby łączyć przełączniki ze sobą kolejne dodajemy po znaku ampersanda - &

Wartości, które można przekazać do filtra:
  • eq - równy
  • ne - nie równy
  • gt - większy od
  • ge - większy bądź równy
  • lt - mniejszy od
  • le - mniejszy lub równy
  • and - operator logiczny "i"
  • or - operator logiczny "lub"
  • Not - zaprzeczenie 
  • Add - matematyczne dodawanie
  • Sub - matematyczne odejmowanie
  • mul - matematyczne mnożenie
  • div - matematyczne dzielenie
  • mod - reszta z dzielenia
  • () - grupowanie wyrażeń
Przykład filtra: /AdventureOData.svc/Customers?$filter=CustomerID gt 10 and CustomerID lt 20

Standard OData oferuje także szereg funkcji umożliwiających na pracę z łańcuchami tekstowymi, datami, czasem, funkcjami matematycznymi oraz typami encji. Spis tych funkcji można znaleźć na stronie http://www.odata.org/documentation/uri-conventions

niedziela, 27 stycznia 2013

Rzucanie wyjątków a wydajność

W internecie jest wiele artykułów na temat zasadności rzucania czy też nie rzucania wyjątków.
Wiele artykułów mówi o tym aby nie rzucać wyjątków, gdyż powoduje to drastyczne obniżenie wydajności aplikacji. Argumentacja tego stanowiska odnosi się do potrzeby zwinięcia stosu wywołań oraz wypełnienia klasy wyjątku.
Czy aby na pewno jest tak, że rzucenie wyjątku spowoduje aż tak ogromne spowolnienie aplikacji, że warto zwrócić kod wyjątku czy też wartość pustą z wywołania np. metody?
W większości przypadków rzucenie wyjątku nie spowoduje znacznego spadku wydajności aplikacji. Jeżeli rzucenie wyjątków w kodzie który tworzymy powoduje duży spadek używalności aplikacji, znaczy to, iż stosujemy wyjątki w nieodpowiedni sposób.

Jednym z możliwych rozwiązań, jest sprawdzanie parametrów wejściowych dla danej funkcji przed przekazaniem ich do niej. Innym rozwiązaniem jest zaimplementowanie metody typu TryX - przykładem może być TryParse. Szczegółowy opis tych wzorców można odnaleźć na stronie http://msdn.microsoft.com/en-us/library/ms229009.aspx

Stosowanie wyjątków jest głównym sposobem raportowania nieprzewidzianych i błędnych sytuacji w kodzie. Należy się tego trzymać i unikać stosowania "zamienników" wyjątków znanych np. z języka C.

poniedziałek, 21 stycznia 2013

70-516 Transakcje

Transakcja to zbiór poleceń, które powinny być wykonane jako pojedyncza operacja, lub w przypadku błędu - wycofane w całości. Transakcja posiada cztery cechy tzw. ACID
  • atomowość (Atomicity) - wszystkie instrukcje muszą być wykonane jako jedna całość, lub też wszystko musi zostać cofnięte do stanu przed wykonywania transakcji 
  • spójność (Consistency) - po wykonaniu transakcji wszystkie więzy integralności muszą zostać zachowane
  • izolacja (Isolation) - w przypadku wykonywania więcej niż jednej transakcji na danych w tym samym czasie, nie powinny one o tym wiedzieć (uzależnione jest to także od poziomu izolacji)
  • trwałość (Durability) - zatwierdzone transakcje nie mogą spowodować uszkodzenia danych w przypadku nagłego braku zasilania.
W przypadku jednoczesnego dostępu do danych, mamy do czynienia z przypadkiem optymistycznym i pesymistycznym dostępu:
  • optymistyczne algorytmy - zakładają brak zakładania blokad podczas dostępu do danych
  • pesymistyczne algorytmy - nakładają blokady podczas dostępu do danych
W wielu aplikacjach kompletna izolacja danych nie ma sensu. Z jednej strony zapewnia ona maksymalny poziom bezpieczeństwa, z drugiej powoduje nie potrzebne przestoje. Jeżeli zmniejszymy jej poziom może dojść do anomalii. Rodzaje anomalii:
  • dirty read - brudne czytanie - transakcja odczytała dane zmienione przez inną nie zatwierdzoną jeszcze transakcję. Problem może się objawić w przypadku, gdy ta nie zatwierdzona transakcja zostanie odwołana. 
  • nonrepeatable read - występuje gdy jedna transakcja odczytuje te same dane dwukrotnie, a pomiędzy tymi odczytaniami inna transakcja modyfikuje te dane.
  • phantom read - podczas wielokrotnych odczytów danych przez jedną transakcję, inne dodają lub usuwają wiersze. W wyniku tego mamy do czynienia z większą bądź mniejszą ilością zwracanych danych po każdym odczycie.
Poziomy izolacji w MS SQL



Opis poszczególnych poziomów:
  • read uncomitted - brak izolacji
  • read committed with locks - poziom domyślny dla MS SQL - nie pozwala odczytywać danych nie zatwierdzonych przez transakcję
  • read committed with snapshots - tylko zatwierdzone zmiany są widoczne w transakcji. Trzymana jest wersja wiersza w pamięci i zostanie użyta tymczasowa baza danych. Zaletą jest brak blokad.
  • repetable read - tryb najlepszy w przypadku gdy w jednej transakcji chcemy odczytać i zapisać dane. Inne transakcje muszą zaczekać do momentu gdy zakończy działanie transakcja z tym poziomem izolacji
  • snapshot - tworzy kopię danych na których będzie pracować transakcja. Istnieje jednak duże prawdopodobieństwo, że podczas zatwierdzania transakcji dojdzie do konfliktu z powodu działania na tych samych danych przez inne transakcje
  • serializable - zakłada blokadę na wierszach; nie można dodawać, usuwać ani modyfikować zablokowanych danych aż do momentu zaakceptowania transakcji. Najbardziej restrykcyjny poziom, należy go używać tylko wtedy gdy jest to koniecznie wymagane

Tworzenie transakcji
W MS SQL tworzymy transakcję w następujący sposób:

SET XACT_ABORT ON --powoduje przechwytywanie wszystkich bledów powyzej poziomu 21, czyli fatalnychBEGIN TRY
  
BEGIN TRANSACTION  --rozpoczecie transakcji
   --operacje na danych
  
COMMIT TRANSACTION --zatwierdzenie transakcjiEND TRYBEGIN CATCH
  
ROLLBACK TRANSACTION --cofniecie zmian wprowadzonych przez transakcje, jezeli zostanie przchwycony wyjatekEND CATCH


W C# tworzymy ją w ten sposób:

Code:
            using (var connection = new SqlConnection(connectionStringSettings.ConnectionString))
            {
                connection.Open();
                using (var transaction = connection.BeginTransaction())
                {
                    try
                    {
                        using (var command = connection.CreateCommand())
                        {
                            command.Transaction = transaction;
                            //Operacje
                        }
                        transaction.Commit();
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();
                        Console.WriteLine(ex.Message);
                    }
                }
            }


Poziom izolacji w SQLu ustawiamy za pomocą komendy:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

W C# ustawiamy poziom izolacji w metodzie BeginTransaction.


TransactionScope
Klasa ta pozwala w prosty sposób tworzyć bloki transakcyjne dla aplikacji działających z bazą MS SQL.
Kod:
Code:
            using (var transaction = new TransactionScope())
            {
                using (var connection = new SqlConnection(connectionStringSettings.ConnectionString))
                {
                    connection.Open();
                    using (var command = connection.CreateCommand())
                    {
                        //Operacje
                    }
                    transaction.Complete();
                }
            }

Kod sporo uproszczony, jednak dostępny tylko dla bazy MS SQL. W przypadku innych baz należy korzystać z wcześniej podanego rozwiązania

niedziela, 20 stycznia 2013

70-516 Pobieranie i zapis danych do bazy - DbCommand, DbDataAdapter, MARS, UDT

Wykonywanie poleceń na bazie danych najlepiej przeprowadzić przy użyciu klasy DbCommand. Klasa ta do swojego działania wymaga otwartego połączenia. Do połączenia można dodać parametry z wartościami. W zależności od providera parametry mogą wymagać poprawnych nazw.
DbCommand posiada następujące metody:
  • ExecuteNonQuery - wywołanie metody, która nie zwraca rezultatu - jedyną odpowiedzią jako otrzymamy w tym przypadku od serwera bazy danych to ilość zmienionych rekordów
  • ExecuteReader - zwraca klasę umożliwiającą odczyt danych w jedną stronę. Obiekt który zostanie zwrócony po wywołaniu tej metody to DbDataReader. Klasa ta posiada metodę Read, która zwraca czy są jeszcze dane do odczytania. Jeżeli nie otrzymamy wartość null.
  • ExecuteScalar - pozwala na odebranie pojedynczej wartości 

MARS - Multiple Active Result Sets
Ustawienie tej metody do połączenia z bazą danych umożliwia uruchamianie wielu DbDataReader-ów jednocześnie. Jeżeli nie włączymy tej technologi, na raz na serwerze będzie mógł działać tylko jeden DbDataReader na raz. Aby włączyć tę technologię należy do connection string dodać: MultipleActiveResultSets=True
Bez MARS można sobie poradzić tworząc większą ilość połączeń. Ta technologia w zamierzeniu ma ułatwić programiście tworzenie wielu zapytań do bazy danych, wykonywanych w tym samym czasie.


DbDataAdapter
Obiekt ten służy do pobierania i uaktualnia danych pomiędzy bazą a obiektem DataTable. DbDataAdapter posiada właściwości umożliwiające pobieranie, usuwanie, uaktualnianie i usuwanie danych z bazy:
  • SelectCommand
  • InsertCommand
  • UpdateCommand
  • DeleteCommand
DataAdapter sam zarządza połączeniem - otwiera i zamyka je automatycznie. Za pomocą metody Fill pozwala załadować dane do obiektów DataTable, DataSet. Chcąc korzystać z tych poleceń należy je uzupełnić albo ręcznie albo za pomocą obiektu DbCommandBuilder.
Aby zwiększyć wydajność zapisu danych do bazy danych, można ustawić właściwość UpdateBatchSize. Domyślnie wysyłany jest na raz jeden rekord do bazy. Zwiększając tę wartość, na raz zostanie wysłana większa ilość rekordów.


UDTs
Począwszy od MS SQL 2005 jest możliwość używania .NET w środowisku MS SQL. Dzięki temu, możemy napisać kod w C# który zostanie uruchomiony na serwerze. Dzięki wsparciu dla SQLCLR użytkownik może tworzyć własne typy i struktury danych, które zostaną przechowane na serwerze.
Należy pamiętać, że po stronie zarówno klienta, jak i serwera muszą być te same biblioteki DLL zawierające informacje o typach.
Przykładowo stworzymy program, który może przechowywać dane geograficzne (położenie):
Definicja tabeli w bazie danych:

CREATE TABLE Locations(ZipCode CHAR(5),Location Geography);

Następnie w projekcie dodajemy referencję do biblioteki Microsoft.SqlServer.Types. Na mojej maszynie biblioteka ta znajduje się pod ścieżką C:\Program Files (x86)\Microsoft SQL Server\110\SDK\Assemblies

Kod w C# pozwalający na dodanie informacji o położeniu:

Code:
            using (var connection = new SqlConnection(db2ConnectionString))
            {
                var command = connection.CreateCommand();
                command.CommandText = "INSERT INTO Locations VALUES(@zip, @loc)";
                var zip = new SqlParameter("@zip", "34120");
                var loc = new SqlParameter("@loc", SqlDbType.Udt);
                loc.UdtTypeName = "geography";
                loc.Value = SqlGeography.STGeomFromText(new SqlChars("POINT(41.44 78.21)"), 4326);
                command.Parameters.AddRange(new[] {zip, loc});
                connection.Open();
                command.ExecuteNonQuery();
            }

sobota, 19 stycznia 2013

70-516 Połączeniowy model danych - łączenie do źródeł

Za pomocą tzw. providerów możemy łączyć się do różnych źródeł danych w celu pobrania z nich danych, jak i zapisania ich.
Microsoft udostępnia kilka gotowych implementacji, jak i gamę interfejsów umożliwiających napisanie własnego providera. Dostępne providery:
  • OleDb - SQL Server, SyBase, DB2/400. Microsoft Access
  • Odbc - połączenie do wielu źródeł danych
  • SQL Server - MS SQL
W sieci można znaleźć wiele gotowych providerów.


Obiekt połączenia  - DbConnection
Aby móc pobierać i wysyłać dane należy stworzyć połączenie do źródła danych. Podczas tworzenia obiektu połączenia należy podać ConnectionString, który wskazuje gdzie znajduje się źródło i przekazuje dodatkowe parametry. Po zakończeniu pracy z źródłem, połączenie należy zamknąć aby zwolnić używane zasoby.
Przykład stworzenia połączenia:

Code:
            var sqlConnection = new SqlConnection("Server=localhost;Database=AdventureWorks;Trusted_Connection=True;");
            sqlConnection.Open();
            //Operacje na połączeniu
            sqlConnection.Close();

Warto sobie zapisać adres strony http://www.connectionstrings.com. Zawiera ona potrzebne dane do połączenia z bazami danych, plikami danych oraz innymi źródłami.


Przechowywanie ConnectionString-a
ConnectionString najlepiej przechowywać w jednej z podanych lokalizacji:
  • app.config
  • web.config
  • machine.config
Pliki te to XMLe. Dzięki temu w przypadku zmian, nie będzie wymagana ponowna kompilacja oprogramowania.
Dodatkowo dzięki klasie ConfigurationManager w łatwy sposób wydobędziemy informacje potrzebne do połączenia z bazą danych. 
Przykład:

Code:
  <connectionStrings>
    <clear/>
    <add name="db" providerName="System.Data.SqlClient" connectionString="Server=localhost;Database=AdventureWorksLT;Trusted_Connection=True;"/>
  </connectionStrings>

Pobranie tak zdefiniowane ConnectionString-a:

Code:
            var connectionStringSettings = ConfigurationManager.ConnectionStrings["db"];
            using (var connection = new SqlConnection(connectionStringSettings.ConnectionString))
            {
                //operacje
            }




Connection Pool
Tworzenie połączenia do bazy danych jest kosztowne. Aby zaradzić temu problemowi oferowany jest connection pool. Wyobrazić sobie można go jako cache o określonej wielkości. Gdy aplikacja potrzebuje połączenia, sprawdza czy w connection poolu istnieje wolne. Jeżeli tak zwraca je. Jeżeli nie istnieje wolne, ale nie osiągnięto jeszcze maksymalnej wielkości app poola, połączenie zostanie stworzone i zwrócone. Ostania ewentualność to brak wolnych połączeń i osiągnięcie maksymalnej wielkości connection poola. W takim przypadku żądanie o połączenie zostanie umieszczone w kolejce i po zwolnieniu przez inną aplikację połączenia - przydzielone do oczekującej aplikacji.
Connection pool kontrolowany jest przez parametry przekazywane do connection string-a. Możemy między innymi ustalić minimalną i maksymalną wielkość cache, czy połączenie ma być resetowane.

Aby connection pool działał, muszą być spełnione następujące warunki:
  • connection string dla każdego połączenia musi być takie same (dotyczy także wielkości znaków)
  • user id dla każdego procesu musi być takie same (jeżeli nie logujemy się za pomocą loginu i hasła, zostanie pobrany login konta systemowego)
  • ID procesu musi być taki sam
Jak widać z powyższej listy mamy do czynienia z wieloma ograniczeniami. Kiedy więc connection pool jest najbardziej opłacalny? W przypadku aplikacji webowych, gdzie łączymy się do serwera bazy danych zawsze za pomocą tego samego użytkownika.
Fakty odnoście connection pool:
  • Cache tworzony jest po stronie klienta aplikacji - aplikacja serwerowa nie wie o nim. 
  • connection pool tworzony jest po pierwszym utworzeniu połączenia i otarciu go
  • połączenie w connection pool przebywa domyślnie od 4 - 8 minut. Jeżeli chcemy, aby zawsze było dostępne jedno lub więcej należy zdefiniować tę liczbę w connection stringu
  • domyślnie connection pool ustawiony jest na maksymalnie 100 połączeń. Ilość użytych połączeń można sprawdzić w narzędziu Performance Monitor
  • connection pool można wyłączyć, jednak nie jest to zalecane i może znacząco obniżyć wydajność naszej aplikacji
  • czyścić connection pool możemy za pomocą statycznych metod połączenia: ClearPool oraz ClearAllPools

piątek, 18 stycznia 2013

70-516 Serializacja obiektów ADO.NET, bindowanie do kontrolek

W tym artykule skupimy się na serializacji obiektów ADO.NET, przechowywaniu w kolumnach typów złożonych oraz bindowaniu w aplikacjach do danych. 

Serializacja
Obiekt DataTable możemy zapisać do pliku XML, jak i zwykłego strumienia. Metoda która do tego służy to WriteXml:

Code:
simpleDataSet.Tables["Client"].WriteXml("plik.xml");

Nazwa pliku nie może zawierać spacji, a jeżeli nawet podalibyśmy nazwę ze spacjami zostanie ona zmieniona na znak podkreślenia - "_".
Zawartość generowanego XMLa można kontrolować za pomocą własnej schemy dla XMLa oraz ustawiając odpowiednie właściwości kolumn obiektu DataTable.
Za pomocą właściwości ColumnMapping możemy ustawić czy dana kolumna ma być atrybutem, elementem czy też w ogóle może się nie pojawić:

Code:
clientTable.Columns["Id"].ColumnMapping = MappingType.Attribute;

Możliwe wartości typu wyliczeniowego ColumnMapping:
  • Attribute - kolumna jako atrybut
  • Element - kolumna jako element
  • Hidden - dane z tej kolumny nie zostaną zapisane do XMLa
  • SimpleContent - dane zapisane są jako zwykły string bez dodatkowych tagów

Po zapisaniu danych należy zapisać schemę - XSD. Plik ten zawiera także XML, który jest definicją typów danych użytych w DataTable:

Code:
simpleDataSet.Tables["Client"].WriteXmlSchema("schema.xml");

Dane możemy także zapisać razem ze schemą tabeli w jednym pliku. Wybór należy do nas.
Dane wczytuje za pomocą metody ReadXml. Jeżeli schema była w innym pliku wczytujemy ją za pomocą metody ReadXmlSchema:

Code:
            DataTable dt = new DataTable();
            dt.ReadXmlSchema("schema.xml");
            dt.ReadXml("plik.xml");


Obsługa DataSet-u przebiega w ten sam sposób. Przykładowe zapisanie i wczytanie danych z plików XML wygląda następująco:

Code:
            simpleDataSet.WriteXml("dataset_data.xml");
            simpleDataSet.WriteXmlSchema("dataset_schema.xml");

            DataSet ds = new DataSet();
            ds.ReadXmlSchema("dataset_schema.xml");
            ds.ReadXml("dataset_data.xml");



DataSet DiffGram
DiffGram to zserializowany DataSet do pliku zawierający dane o wersji wiersza. Rozwiązanie to może się przydać w przypadku gdy rzadko łączymy się z bazą danych i przechowujemy DataSet lokalnie na dysku. Jeżeli dokonamy zmian, następnie zapiszemy je do pliku a później odczytamy, stracimy informacje o zmianach. Nie będziemy w stanie uaktualnić danych w bazie danych.
Przykład tworzenia DiffGram-a:

Code:
simpleDataSet.WriteXml("Diff_Gram.xml", XmlWriteMode.DiffGram);

Wczytując (deserializując) dane należy podać odpowiednią wartość XmlReadMode:
  • Auto - plik XML zostanie sprawdzony i zostanie wybrana najlepsza metoda odczytu
  • DiffGram - zachowuje się w taki sam sposób jak metoda Merge
  • Fragment - wczytuje plik XML jako część, oznacza to że plik XML może zawierć wiele elementów głónych
  • IgnoreSchema - nie deserializuje schematu
  • InferSchema - schemat DataSet-u jest tworzony na podstawie danych zawartych w pliku
  • InferTypedSchema - schemat DataSet-u tworzony jest na podstawie danych zawartych w pliku, jeżeli nie jest w stanie jednoznacznie wyznaczyć typu - użyty zostanie typ string
  • ReadSchema - wczytuje dane wraz z schematem


Binarna serializacja DataSet 
Serializacja binarna przydatna jest w przypadku gdy chcemy aby plik wynikowy był jak najmniejszy. Serializacja przebiega w taki sam sposób. Należy jednak wcześniej ustawić właściwość RemotingFormat na Binary. Jeżeli tego nie zrobimy, plik wynikowy będzie zawierał w swoim wnętrzu plik XML.
Serializacja powinna wyglądać w następujący sposób:

Code:
            simpleDataSet.RemotingFormat = SerializationFormat.Binary;
            using (var fs = new FileStream("dane.bin", FileMode.Create))
            {
                var binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(fs, simpleDataSet);
            }



Odczyt danych za pomocą DataTableReader
Dane z DataTable oraz DataSet można odczytać za pomocą klasy DataTableReader. Klasa ta pozwala tylko na odczyt wierszy za pomocą metody Read. Jeżeli wiersz zostanie dodany w trakcie iteracji po danych to w zależności od tego czy zostanie dodany przed wskaźnikiem aktualnej pozycji czy też za nim zostanie wyświetlony bądź też nie. Kiedy DataTableReader dojdzie do końca tabeli, metoda Read zwróci null. Po tej operacji dodanie danych nie spowoduje ich odczytania.
Jeżeli iterujemy DataSet, dane będą odczytywane najpierw z pierwszej tabeli a następnie za pomocą metody NextResult możemy przejść do kolejnej tabeli.
Przykład odczytu DataSet:

Code:
            DataTableReader dr = simpleDataSet.CreateDataReader();
            while (dr.Read())
            {
                Console.WriteLine(dr["Id"] + " " + dr["FirstName"]);
            }
            dr.NextResult();
            while (dr.Read())
            {
                Console.WriteLine(dr["Id"] + " " + dr["CreationDate"]);
            }



Przechowywanie typów złożonych w kolumnach DataTable
W kolumnie DataTable można przechowywać typy specjalne, takie jak obiekty klasy, dane XML. Kolumna taka może przechować dowolny typ który jest referencyjny - wyjątkiem jest klasa string.
Przykład w jaki sposób można przechować w kolumnie obiekty typu Computer w tabeli User:

Code:
            DataTable dtSpecializedColumn = new DataTable("User");
            dtSpecializedColumn.Columns.Add("Id", typeof (int));
            dtSpecializedColumn.Columns.Add("FirstName", typeof (string));
            dtSpecializedColumn.Columns.Add("LastName", typeof (string));
            dtSpecializedColumn.Columns.Add("Computer", typeof (Computer));

            dtSpecializedColumn.Rows.Add(new object[]
                            {
                                1, "Jacek", "Kowalski",
                                new Computer {Id = 1, GraphicCard = "Ati", Processor = "Intel", UsbAmount = 10}
                            });
            Console.WriteLine(((Computer)dtSpecializedColumn.Rows[0]["Computer"]).UsbAmount);



Bindowanie w WindowsForms
Kontrolki w WindowsForms można bindować do obiektów implementujących IList, IListSource, IBindingList, IBindingListView. Właściwości które używamy podczas bindowania:
  • DataSource - źródło implementujące jeden z wcześniej wymienionych interfejsów
  • DataMember - jeżeli źródło zawiera wiele elementów np. DataSet zawiera wiele obiektów DataTable, należy wskazać, którą tabelę chcemy wyświetlić. 
  • DisplayMember - dla kontrolek typu lista wskazuje która kolumna ma być wyświetlana 
  • ValueMember - wskazuje, wartość która zostanie pobrana z kontrolki typu lista; najczęściej jest to klucz główny

Bindowanie w ASP.NET
W przypadku ASP.NET, właściwości w kontrolkach webowych mają takie same znaczenie jak dla WindowsForms.
Należy pamiętać, że po bindowaniu musi zostać wywołana metoda DataBind. Jeżeli tego zrobimy dane nie zostaną dodane do kontrolki.


Bindowane w WPF
W przypadku WPFa możemy bindować do praktycznie dowolnego obiektu. Główną właściwością do której następuje bindowanie jest ItemsSource.


Źródła do pobrania: https://skydrive.live.com/redir?resid=BCCBFB57B3C672D0!150&authkey=!AJunqZlTIcRyX-s

czwartek, 17 stycznia 2013

70-516 DataSet

DataSet można sobie wyobrazić jako pamięciową bazę danych. Obiekt ten nie oferuje oczywiście takich aspektów jak transakcje.
Obiekt ten przechowuje w sobie tabele jako obiekty klasy DataTable oraz relacje pomiędzy nimi przedstawione jako obiekty DataRelation.
DataSet umożliwia ponadto:
  • kopiowanie danych jak i schematu do nowych DataSet-ów
  • łączenie z innym DataSet-em
  • śledzenie i reagowanie na zmiany
DataSet można stworzyć zarówno w kodzie jak i dostarczając XML z konfiguracją.
Przykład tworzenia DataSet-a zawierającego tabele klientów i zamówień, oraz połączenie pomiędzy tymi tabelami:

Code:
            var clientDataSet = new DataSet("ClientOrders");
            
            var clientTable = new DataTable("Client");
            clientTable.Columns.Add(new DataColumn("Id", typeof (int)));
            clientTable.Columns.Add("FirstName", typeof (string));
            clientTable.Columns.Add("LastName", typeof(string));
            clientTable.Columns.Add("BirthYear", typeof(int));
            clientTable.PrimaryKey = new DataColumn[] {clientTable.Columns["Id"]};

            var orderTable = new DataTable("Order");
            orderTable.Columns.Add("Id", typeof (int));
            orderTable.Columns.Add("CreationData", typeof (DateTime));
            orderTable.Columns.Add("Price", typeof (decimal));
            orderTable.Columns.Add("ClientId", typeof (int));
            orderTable.PrimaryKey = new DataColumn[] {orderTable.Columns["Id"]};

            clientDataSet.Tables.AddRange(new DataTable[] {clientTable, orderTable});
            clientDataSet.Relations.Add("client_order", clientTable.Columns["Id"], orderTable.Columns["ClientId"]);


Lepszym rozwiązaniem od powyższego, jest stworzenie tzw. typowanego DataSet-u. Tworzymy go dziedzicząc po klasie DataSet a następnie tworząc właściwości odpowiadające poszczególnym tabelą. Zaletą tego rozwiązania jest późniejsze wsparcie IntelliSense podczas odnoszenia się do tabel.

Drugim sposobem tworzenia typowanego DataSet-a jest stworzenie pliku XML zawierającego specyfikację. XML-a można stworzyć ręcznie ale istnieje też narzędzie umożliwiające graficzne tworzenie reprezentacji DataSet-a. Aby uruchomić narzędzie wystarczy dodać do projektu nowy plik typu DataSet:







Po dodaniu nowych plików zostanie otwarty edytor gdzie w graficzny sposób stworzymy schemat:





Relacje
Za relacje pomiędzy tabelami odpowiada obiekt typu DataRelation. Za pomocą obiektu relacji możemy nawigować w obie strony: od tabeli nadrzędnej do podrzędnej jak i od podrzędnej do nadrzędnej.
Przykład nawigacji (na końcu zostanie udostępniony kod do pobrania z całością omawianego tutaj materiału):

Code:
            //1. Od tabeli nadrzędnej do podrzędnej
            var childRows = simpleDataSet.Tables["Client"].Rows[0].GetChildRows("client_order");
            foreach (var childRow in childRows)
            {
                Console.WriteLine("{0} {1} {2}", childRow["Id"], childRow["CreationDate"], childRow["Price"]);
            }
            Console.WriteLine("----------------------------");
            //2. Od podrzędnej do nadrzędnej
            var parentRow = simpleDataSet.Tables["Order"].Rows[0].GetParentRow("client_order");
            Console.WriteLine("{0} {1} {2}", parentRow["Id"], parentRow["FirstName"], parentRow["LastName"]);


Klucze obce
Podczas tworzenia relacji stworzyliśmy klucz obcy w tabeli Order. Za tworzenie klucza odpowiada jeden z parametrów funkcji Add dla kolekcji relacji:

Code:
clientDataSet.Relations.Add("client_order", clientTable.Columns["Id"], orderTable.Columns["ClientId"], true);

Domyślnie parametr ten jest ustawiony na true czyli klucz obcy jest tworzony za każdym razem.


Kaskadowe uaktualnianie i usuwanie danych
Klucze obce wymuszają zachowanie integralności pomiędzy tabelą nadrzędną a podrzędną. Wiersze podrzędne nie mogą zostać stworzone, dopóki wiersz nadrzędny nie będzie istniał. W bazach danych jak i w obiekcie DataRelation można ustawić tryb kaskadowego usuwania danych. W tym trybie, gdy zostanie usunięty wiersz z tabeli nadrzędnej, wiersze z tabeli podrzędnej zostaną usunięte automatycznie.
Typ wyliczeniowy Rule zawiera inne możliwości dla reguł usuwania jak i uaktualnia danych:
  • Cascade - domyślne ustawienie - uaktualnia lub kasuje potomne wiersze w momencie zmiany unikalnego klucza, bądź usunięcia wiersza nadrzędnego 
  • None - spowoduje rzucenie wyjątku w przypadku próby usunięcia wiersza nadrzędnego lub próby zmiany jego wartości
  • SetDefault - ustawia wartość domyślną dla kolumny klucza obcego
  • SetNull - ustawia null dla klucza obcego
Aby ustawić inną wartość dla klucza głównego, np. aby rzucał wyjątkiem, jeżeli w tabeli istnieją podrzędne wiersze, należy pobrać klucz główny i zmienić jego właściwość DeleteRule/UpdateRule:

Code:
            ((ForeignKeyConstraint)orderTable.Constraints["client_order"]).DeleteRule = Rule.None;
            ((ForeignKeyConstraint)orderTable.Constraints["client_order"]).UpdateRule = Rule.None;



Łączenie DataSet-ów
Przykładem zastosowania powyższej operacji jest pobranie z bazy danych, a następnie dodanie danych pobranych z pliku. DataSet umożliwia łączenie danych z obiektami typu DataSet, DataTable oraz DataRow za pomocą metody Merge:

Code:
            var copyOfDataset = simpleDataSet.Copy();
            copyOfDataset.Tables["Client"].Rows[2]["FirstName"] = "Kamil";
            copyOfDataset.Tables["Order"].Rows.Add(9, DateTime.Parse("2013-01-26"), 256.78, 2);

            simpleDataSet.Merge(copyOfDataset, false, MissingSchemaAction.AddWithKey);

Parametry które przyjmuje metoda Merge:
  • pierwszy parametr to DataSet z którym ma nastąpić połączenie
  • drugi parametr mówi o tym czy zmiany dokonane w drugim DataSet mają nadpisać dane w pierwszym. W podanym wyżej przykładzie wiersz gdzie zmieniamy imię na Kamil nie ma stanu Unchanged - w takim przypadku flaga ustawiona na true sprawiłaby, że dane nie zostały by uaktualnione
  • trzeci parametr to wartość typu wyliczeniowego MissingSchemaAction - ustawienie to odpowiada w jaki sposób mają zostać potraktowane nowe dane.
Typ wyliczeniowy MissingSchemaAction:
  • Add - dodaje wymagane obiekty DataTable oraz DataColumn do schematu
  • AddWithKey - dodaje tabele, kolumny oraz klucz główny
  • Error - jeżeli dana kolumna nie istnieje w DataSecie docelowym, zostanie rzucony wyjątek
  • Ignore - dane przechowywane w kolumnach, które nie występują w źródle nie zostaną dodane
Należy pamiętać o tym aby dodawać klucz główny do tabeli, którą zamierzamy łączyć z naszym DataSetem. Jeżeli tego nie zrobimy, wiersze zamiast ulec uaktualnieniu zostaną powielone (wiersze z drugiej tabeli zostaną skopiowane jako nowe rekordy).

Link do pobrania źródeł: http://sdrv.ms/ZYw9oE

środa, 16 stycznia 2013

70-516 DataView

DataView umożliwia sortowanie i filtrowanie danych zawartych w obiekcie DataTable. Jeden obiekt DataTable może posiadać wiele widoków (DataView), dzięki czemu nie ma potrzeby za każdym razem pobierać danych z źródła danych jakim np. jest baza danych.

Sortowanie
Sortowanie konfigurujemy w bardzo prosty sposób. Do zmiennej Sort przypisujemy kolumny po których chcemy przeprowadzić sortowanie i podajemy kierunek: rosnąco - ASC, malejąco - DSC. Jeżeli chcemy sortować po więcej niż jednej kolumnie, kolejne definicje sortowania wprowadzamy po przecinku:

Code:
dataView.Sort = "tytul ASC, rezyser DESC";


Filtrowanie wyników
Wyniki możemy filtrować zarówno pod względem zawartości, jak i stanu wiersza.
Właściwość, która pozwala filtrować wiersze pod względem zawartości danych jest RowFilter. Składnia właściwi jest taka sama jak w SQLu. Przykład:

Code:
dataView.RowFilter = "nosnik = 'dvd' and tytul like '%a%'";

Właściwość RowStateFilter pozwala przeszukiwać dane według stanu wiersza obiektu DataTable. Właściwość ta przyjmuje wartości zawarte w typie wyliczeniowym DataViewRowState.
Możliwe wartości:
  • Added 
  • CurrentRows
  • Deleted
  • ModifiedCurrent
  • ModifiedOriginal
  • None - czyści filtr
  • OriginalRows
  • Unchanged

Przykład:

Code:
dataView.RowStateFilter = DataViewRowState.Unchanged;


Iteracja po obiekcie DataView
Jest bardzo podobna do iteracji po obiekcie DataTable:

Code:
            foreach (DataRowView row in dataView)
            {
                for (int i = 0; i < dataView.Table.Columns.Count; i++)
                {
                    Console.Write(row[i] + "  ");
                }
                Console.WriteLine();
            }



Eksport DataView do DataTable
Operacja ta może się przydać, gdy chcemy stworzyć nowy obiekt DataTable np. na podstawie zawężonych danych (użytkownik ustawi filtrowanie, sortowanie itd.). Dodatkowo możemy zdefiniować kolumny które będą zawarte w nowej tabeli. Przeprowadzamy ją za pomocą metody ToTable().
Przykład:

Code:
var newTable = dataView.ToTable("NewTable", false, "nosnik", "tytul");

sobota, 12 stycznia 2013

70-516 Bezpołączeniowy model cz. 2

W poprzednim poście rozpocząłem omawianie obiektu DataTable. W tym poście kontynuuje rozpoczęty temat.

Stan wiersza
Wiersze w obiekcie DataTable zmieniają swoje podczas całego życia obiektu. Sprawdzając właściwość RowState możemy dowiedzieć w jakim stanie jest obecnie wiersz. Wartości tego typu wyliczeniowego:
  • Detached - wiersz został stworzony, ale nie dodany do tabeli
  • Added - wiersz został stworzony i dodany do tabeli
  • Unchanged - stan wiersza nie zmienił się od ostatniego wywołania metody AcceptChanges; w momencie wywołania metody AcceptChanges wiersz przyjmuje ten stan
  • Modified - wiersz zmienia się w ten stan tylko jeżeli poprzednio miał stan Unchanged; po wywołaniu AcceptChanges wiersz zmienia swój stan na Unchanged
  • Deleted - wiersz przyjmuje ten stan gdy zostanie usunięty z tabeli
Przykład obrazujący zmianę stanów wiersza:

Code:
        static void Main(string[] args)
        {
            var clientTable = new DataTable("Client");
            var idColumn = new DataColumn("Id");
            idColumn.DataType = typeof (int);
            idColumn.Unique = true;
            idColumn.AllowDBNull = false;
            idColumn.Caption = "ID";
            idColumn.AutoIncrement = true;
            idColumn.AutoIncrementSeed = 1;
            idColumn.AutoIncrementStep = 1;
            clientTable.Columns.Add(idColumn);

            var firstNameColumn = new DataColumn("FirstName");
            firstNameColumn.DataType = typeof (string);
            firstNameColumn.MaxLength = 50;
            firstNameColumn.AllowDBNull = true;
            firstNameColumn.Caption = "Imię";
            clientTable.Columns.Add(firstNameColumn);

            clientTable.PrimaryKey = new DataColumn[] {idColumn};

            //Add new row by creating them first
            var row1 = clientTable.NewRow();
            row1["FirstName"] = "Jacek";
            Console.WriteLine(row1.RowState);
            clientTable.Rows.Add(row1);
            Console.WriteLine(row1.RowState);
            row1["FirstName"] = "Marek";
            Console.WriteLine(row1.RowState);
            row1.AcceptChanges();
            Console.WriteLine(row1.RowState);
            row1["FirstName"] = "Sebastian";
            Console.WriteLine(row1.RowState);
            row1.RejectChanges();
            Console.WriteLine(row1.RowState);
            Console.WriteLine(row1["FirstName"]);
            row1.Delete();
            Console.WriteLine(row1.RowState);
        }



Zarządzanie danymi w wierszu za pomocą wersji wiersza
DataTable może posiadać do trzech wersji danego wiersza: oryginalny (Original), obecny (Current) oraz zaplanowany (Proposed). Możliwe wartości przechowywane są w enumie DataRowVersion:
  • Current - obecna wartość; istnieje zawsze oprócz sytuacji w której usuniemy wiersz (próba pobrania zakończy się wyjątkiem)
  • Default - jeżeli DataRowState jest ustawiony na Added lub Modified - domyślna wartość to Current; jeżeli Deleted - rzucony zostanie wyjątek; jeżeli zostanie wywołana metoda BeginEdit - Proposed
  • Original - wartość załadowana do wiersza, lub ta od ostatniego wywołania metody AcceptChanges. Wartość ta nie jest ustawiana dopóki wiersz nie znajdzie się w stanie Modified, Unchanged, lub Deleted. Jeżeli wiersz jest w stanie Added, zostanie rzucony wyjątek VersionNotFound
  • Proposed - wartość w trakcie edycji wiersza. Jeżeli wiersz jest w stanie Deleted, zostanie rzucony wyjątek. Jeżeli metoda BeginEdit nie została wywołana, bądź też została wywołana na wierszu, który nie należy do DataTable, zostanie rzucony wyjątek VersionNotFound
Za pomocą metody HasVersion możemy w bezpieczny sposób sprawdzić czy dana wersja jest dostępna. Przykładowy kawałek kodu pozwalający na bezpieczne pobranie danej wersji wiersza:

Code:
            foreach (var rowVersion in Enum.GetNames(typeof(DataRowVersion)))
            {
                var enumValue = (DataRowVersion)Enum.Parse(typeof (DataRowVersion), rowVersion);
                if (row1.HasVersion(enumValue))
                {
                    Console.WriteLine(enumValue + " " + row1["FirstName", enumValue]);
                }
            }


Metody AcceptChanges oraz RejectChanges
Metody te, jak same ich nazwy mówią służą do akceptacji lub odrzucenia danych. Metody te są potrzebne, aby zmienić stan wiersza na Unchanged.
Po dokonaniu zmian wierszy, które zostały pobrane z źródła danych (np. bazy danych) należy wybrać tylko te wiersze, których stan to Modified. Wiesze które zostały zmodyfikowane możemy pobrać za pomocą metody GetChanges. Po zapisaniu zmian do źródła danych należy wywołać metodę AcceptChanges w celu zresetowania stanu wierszy przechowywanych w obiekcie DataTable.
Wywołanie AcceptChanges dodatkowo powoduje skopiowanie danych z wersji Current do wersji Original.
Wywołanie RejectChanges powoduje skopiowanie danych z wersji Original do Current.

Stan wiersza możemy także zmienić za pomocą dwóch dodatkowych metod:
  • SetAdded - dodany
  • SetModified - zmodyfikowany
Operacja taka może być przydatna gdy chcemy dane zapisać do innego źródła danych niż z którego oryginalnie zostały pobrane. Wystarczy w takim przypadku wywołać metodę SetAdded i przesłać taki obiekt DataTable do DataAdapter-a.
Metody te mają ograniczenie do wywoływania ich na wierszach, które są w stanie Unchanged. Jeżeli wiersz jest w innym stanie, zostanie rzucony wyjątek InvalidOperationExcetpion.
Dodatkowo metody te:
  • SetAdded - usuwa oryginalną wersjędanych
  • SetModified - nie modyfikuje wersji wiersza

Usuwanie wierszy i cofanie tej operacji
Wywołując metodę Delete na wierszu oznaczamy go do usunięcia. Następstwem jest usunięcie wersji danych Current oraz Proposed.
Jeżeli okaże się, że dany wiersz ma nie być usunięty, można użyć metody RejectChanges. Dane Original zostaną skopiowane do Current. Wiesz wraca do stanu sprzed ostatniego wywołania metody AcceptChanges, jednak wszystkie operacje wykonane po tej metodzie nie zostaną przywrócone.

Iteracja po danych zawartych w DataTable
Po obiekcie DataTable można iterować za pomocą pętli:

Code:
            var productTable = new DataTable("Products");
            productTable.Columns.Add("Id", typeof (int));
            productTable.Columns.Add("Name", typeof(string));
            productTable.Columns.Add("Price", typeof(decimal));
            productTable.Columns.Add("Amount", typeof(int));
            productTable.Rows.Add(new object[] { 1, "Laptops", 1678, 100 });
            productTable.Rows.Add(new object[] { 2, "Printers", 234.6d, 40 });
            productTable.Rows.Add(new object[] { 3, "Keybords", 70.30d, 130 });

            foreach (DataRow row in productTable.Rows)
            {
                foreach (DataColumn column in productTable.Columns)
                {
                    Console.Write(row[column.ColumnName] + "  ");
                }
                Console.WriteLine();
            }



Kopiowanie obiektów DataTable
Obiekty DataTable można kopiować za pomocą dwóch metod:
  • Copy - tworzy kopię zawierającą schemat i dane
  • Clone - kopiuje tylko schemę
Jeżeli zdecydujemy się na metodę Clone, możemy za pomocą metody ImportRow skopiować do niej wybrane rekordy (należy pamiętać, że schema tabel musi być taka sama). W przypadku gdy do tabeli docelowej będziemy próbowali skopiować rekord z kluczem głównym, który już w niej występuje, zostanie wyrzucony wyjątek ConstraintException.


Przykładowy kod do pobrania: http://sdrv.ms/X0ZixZ

piątek, 11 stycznia 2013

70-516 Bezpołączeniowy model

Zacznę od wyjaśnienia samego tytułu, gdyż może być mylący. Stwierdzenie "bezpołączeniowy model" mogłoby sugerować, iż między źródłem danych a samymi klasami nie występuje zależność. Nic bardziej mylnego. Bezpołączeniowy model oznacza, iż klasy odpowiedzialne za obsługę danych są w stanie działać bez ciągłego połączenia z źródłem danych. Jeżeli będziemy chcieli pobrać świeżą paczkę danych, klasy te nawiążą połączenie, pobiorą potrzebne dane, a następnie zakończą połączenie.

Spójrzmy na diagram:


Przedstawiony powyżej diagram przedstawia klasy, wchodzące w skład bezpołączeniowego i połączeniowego modelu.
W tym poście zajmiemy się bezpołączeniowym modelem danych.

DataTable
Obiekt klasy DataTable przedstawia tabelę (np. bazy danych) jako obiekt w pamięci. Obiekt ten przechowuje kolumny jako instancje klasy DataColumn oraz wiersze zawierające dane - DataRow. Tabeli możemy nadać nazwę, co w późniejszym czasie ułatwia jej pobranie z kolekcji tabel.
Każda kolumna przechowywana w DataTable może posiadać dodatkowe ograniczenia związane z rodzajem przechowywanych w niej danych. Dzięki temu, zapewniamy integralność przechowywanych danych.
Niektóre właściwości oraz ich domyślne wartości dla obiektu DataColumn:
  • DataType - typ przechowywanych danych - domyślnie string
  • MaxLength - maksymalna długość w przypadku łańcuchów tekstowych - domyślnie -1 co oznacza brak sprawdzania tego parametru
  • Unique - wartości unikalne - domyślnie false czyli brak kontroli przed wstawianiem duplikatów wartości
  • AllowDBNull - wartości DBNull - domyślnie true czyli nie trzeba specyfikować dla danej kolumny wartości
  • Caption - tytuł kolumny wykorzystywany np. w kontrolkach graficznych typu DataGridView - domyślnie przyjmuje wartość ColumnName
Przykład stworzenia obiektu DataTable:

Code:
            var clientTable = new DataTable("Client");
            var idColumn = new DataColumn("Id");
            idColumn.DataType = typeof (int);
            idColumn.Unique = true;
            idColumn.AllowDBNull = false;
            idColumn.Caption = "ID";
            clientTable.Columns.Add(idColumn);

            var firstNameColumn = new DataColumn("FirstName");
            firstNameColumn.DataType = typeof (string);
            firstNameColumn.MaxLength = 50;
            firstNameColumn.AllowDBNull = true;
            firstNameColumn.Caption = "Imię";
            clientTable.Columns.Add(firstNameColumn);

Do tabeli możemy także dodać informacje mówiące która kolumna/kolumny tworzą klucz główny (Primary Key) oraz ustawić automatycznie numerowane kolumny. Klucz główny wskazujemy za pomocą właściwości PrimaryKey:

Code:
clientTable.PrimaryKey = new DataColumn[] {idColumn};

Auto numerowanie konfigurujemy za pomocą trzech właściwości:
  • AutoIncrement - flaga mówiąca czy auto numerowanie jest włączone dla danej kolumny
  • AutoIncrementSeed - wartość od której się rozpocznie auto numerowanie
  • AutoIncrementStep - krok, o który zwiększy się kolejna wartość w stosunku do poprzedniej
Dla kolumny id, auto numerowanie możemy włączyć w następujący sposób:

Code:
            idColumn.AutoIncrement = true;
            idColumn.AutoIncrementSeed = 1;
            idColumn.AutoIncrementStep = 1;

Nasuwa się pytanie: co się stanie jeżeli w bazie danych także dane pole jest ustawione jako auto numerowane? Otóż wartości automatycznie wyliczane nie są przekazywane do bazy. Baza danych sama wyznaczy kolejne wartości dla pola auto numerowanego. Następnie zostanie uaktualnione pole w obiekcie DataTable wartością pobraną z bazy danych. Istnieje jednak ryzyko: jeżeli nasz DataTable jest numerowany np. od 1 do 50 a w tabeli bazodanowej jest już 10 będziemy mieli do czynienia z następującym scenariuszem:
  • do bazy danych zostanie dodany nowy rekord
  • baza danych wyznaczy dla niego wartość id = 11
  • obiekt DataTable będzie chciał uaktualnić wartość pola kolumny id na 11
  • z powodu istnienia w tabeli rekordu z id = 11, zostanie rzucony wyjątek
Jak zabezpieczyć się na taką ewentualność? Można ustawić wartość właściwości AutoIncrementSeed oraz AutoIncrementStep na -1. Dzięki temu będą generowane wartości ujemne. W większości baz danych identyfikatory dla wierszy są wartościami dodatnimi więc nie pojawi się konflikt z istniejącymi wartościami w bazie, a uaktualnianie wartości w DataTable nie spowoduje konfliktów związanych z auto numerowaniem.

Dodawać dane do DataTable możemy za pomocą metody Add, która została przeładowana i przyjmuje dane w dwóch postaciach:
  • jako obiekt typu DataRow
  • jako tablicę wartości dla poszczególnych kolumn - należy wtedy podać wartość dla każdej kolumny
Innymi metodami umożliwiającymi import wierszy do obiektu DataTable są:
  • ImportDataRow - pozwala na import wiersza z zachowaniem jego stanu
  • Load - pozwala zaimportować nowe wiersze bądź uaktualnić wartości w istniejących
  • LoadDataRow - pozwala wygenerować nowy wiersz na podstawie tablicy wartości dla poszczególnych kolumn
Metoda LoadDataRow oraz Load pozwala wybrać sposób w jaki zostaną dodane/uaktualnione dane w wierszu - odpowiada za to enum  LoadOption, który ma następujące możliwości:
  • OverwriteChanges - nadpisuje dane i zmienia stan wiersza na Unchanged; jeżeli zostanie dodany nowy wiersz - także otrzyma stan Unchanged
  • PreserveChanges - domyślny tryb - nadpisuje oryginalne dane nowymi, ale nie zmienia wersji wiersza. Nowe wiersze będą w stanie Unchanged
  • Upsert - zmienia aktualną wersję, ale nie oryginalną. Nowe wiersze będą mieć stan Added; wiersze ze stanem Unchanged pozostaną w takim stanie jeżeli aktualna wersja jest taka sama jak oryginalna, w przeciwnym wypadku wiersze będą w stanie Modified

Poniżej znajduje się przykład dodawania wierszy do obiektu DataTable za pomocą wcześniej omówionych metod.
Code:
            //Add new row by creating them first
            var row1 = clientTable.NewRow();
            row1["FirstName"] = "Jacek";
            var row2 = clientTable.NewRow();
            row2["FirstName"] = "Patryk";
            clientTable.Rows.Add(row1);
            clientTable.Rows.Add(row2);

            //Using add with values
            clientTable.Rows.Add(new object[] {3, "Sebastian"});

            //LoadData
            clientTable.LoadDataRow(new object[] { 4, "Marek" }, LoadOption.OverwriteChanges);

czwartek, 10 stycznia 2013

70-516 Wstęp

Egzamin 70-516 dotyczy wiedzy o dostępie do źródeł danych. Nie ogranicza się on tyko do baz danych, ale skupia swoją uwagę także na XML-u oraz popularnych obecnie webserwisach.
Podstawowym źródłem danych, które zamierzam wykorzystać w celu przygotowania się do egzaminu to przede wszystkim Training Kit 70-516 Accessing Data with Microsoft .NET Framework 4.
Książa przedstawia następujące aspekty dotyczące pracy z danymi:
  • połączeniowy model danych
  • bezpołączeniowy model danych
  • zapytania LINQ
  • LINQ2SQL
  • LINQ2XML
  • Entity Framework
  • WCF Data Services
  • zbieranie informacji o wydajności w ADO.NET
  • synchronizacja danych
  • tworzenie i udostępnianie aplikacji korzystających z źródeł danych
Jak widać książka zawiera bardzo dużo ciekawego materiału.
Jako, że w tematyce dostępu i zarządzaniu danymi czuję się bardzo swobodnie, niektórym tematom poświęcę mniej miejsca. Postaram się jednak pisać jak najbardziej zrozumiale i streszczać długie rozdziały, wybierając z nich najważniejsze informacje. Miłego czytania!

środa, 9 stycznia 2013

70 - 511 Podsumowanie i wrażenia po egzaminie

Do egzaminu przygotowałem się już od dłuższego czasu. Artykuły, które w tym czasie tutaj umieszczałem miały na celu zapisanie notatek, aby na parę godzin przed egzaminem powtórzyć sobie najważniejsze rzeczy.
Pytania dotyczyły głównie możliwości które oferuje WPF - bindowanie, walidacja, szablony, style. Nie zabrakło także pytań dotyczących bezpieczeństwa tworzonych aplikacji.
Z mojej perspektywy pytania były ciekawe i pozwalały zweryfikować, czy ktoś ma do czynienia z programowanie w WPF czy może tylko jest oczytany w tematach.
Przystępowanie do egzaminu po przeczytaniu tylko artykułów na temat WPF nie ma sensu. Należy poświęcić wiele czasu na praktyczną część i dużo ćwiczyć i jeszcze raz ćwiczyć praktycznie pisząc kod.
Czasu na egzamin jest wystarczająco dużo, aby można było go dwukrotnie odbyć. Można więc na spokojnie przemyśleć sobie kawałki kodu i nawet jeżeli nie jest się czegoś pewnym, drogą dedukcji dojść do prawidłowego rozwiązania.
Czy taka forma egzaminu w pełni odzwierciedla wiedzę i umiejętności programisty? Nie mam zamiaru tutaj polemizować z kimkolwiek na ten temat. W każdym razie sama wiedza teoretyczna nie jest wystarczająca, a znajomość i wiedza, że takie narzędzie przydaje się do tego a tamto do tamtego - nie zastąpi praktycznych umiejętności posługiwania się nimi.
Egzamin zaliczony :) czas na kolejne wyzwania. W planach mam zdanie 5 egzaminów do maja. W tym momencie mam już pierwszy za sobą.
Kolejny który zamierzam podjąć to 70 -516 Accessing Data with Microsoft .NET Framework 4 - możecie się spodziewać następnych notatek ;)

poniedziałek, 7 stycznia 2013

Wyszukiwanie w kolumnie zawierającej XML

Jeżeli w tabeli jedna z kolumn zawiera XML, możemy łatwo wybierać dane z tej kolumny (struktury xml) jak i szukać po elementach XML-a. Nie ma potrzeby pobierania całej zawartości tabeli do pamięci w programie, a następnie wybieranie danych za pomocą np. LINQ2XML.

Przykład:

Struktura tabeli w bazie ma następującą postać:

pole detale filmu jest XML-em następującej postaci:

Code:
<Szczegoly>
 <WersjaJezykowa>EN/PL</WersjaJezykowa>
 <audio>5.1</audio>
 <lektor>EN/PL</lektor>
 <rok>2004</rok>
</Szczegoly>

Jeżeli chcemy wyszukać np. film z 1990 roku wystarczy napisać następującego SQL-a:


SELECT * FROM film WHERE detale_filmu.value('(Szczegoly/rok/node())[1]', 'int') = 1990


Język zapytań jaki tutaj został użyty w celu wyciągnięcia zawartości XMLa to XQuery

Bindowanie do zasobów Dynamic vs Static

Tym razem krótko, o czymś co może się przydać nie tylko na egzaminie, ale także w codziennej pracy z zasobami i bindowaniem do nich.

Rozważmy cztery przypadki bindowania do zasobów w kodzie.
Kod wyjściowy dla wszystkich przypadków:

Code:
<Window x:Class="DynamicStatic.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <SolidColorBrush x:Key="Background" Color="Blue" />
        <SolidColorBrush x:Key="Foreground" Color="Red" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Button Content="Przycisk"></Button>
        <Button Name="btnDoSomething" Content="Wykonaj" Grid.Row="1" Click="BtnDoSomething_OnClick" />
    </Grid>
</Window>



Dwa zasoby dla tła i koloru czcionki, będziemy przypisywać do przycisku, a następnie w kodzie pobierzemy i zmienimy ich wartość. Zmieniać będzie się tylko definicja klawisza i metody podpiętej pod zdarzenie drugiego klawisza.

1. Przypisanie statyczne i zmiana koloru pędzla:

Code:
<Button Background="{StaticResource Background}" Foreground="{StaticResource Foreground}" Content="Przycisk"></Button>

private void BtnDoSomething_OnClick(object sender, RoutedEventArgs e)
        {
            var background = (SolidColorBrush)this.FindResource("Background");
            background.Color = Colors.Yellow;

            var foreground = (SolidColorBrush)this.FindResource("Foreground");
            foreground.Color = Colors.Black;
        }

Efekt? Zmiana kolorów:


2. Przypisanie statyczne i zmiana pędzla:
Różnica polega na zmianie pędzla - czyli podmieniamy cały obiekt zasobów (XAML definiujący przycisk nie zmienia się):

Code:
        private void BtnDoSomething_OnClick(object sender, RoutedEventArgs e)
        {
            this.Resources["Background"] = new SolidColorBrush(Colors.Yellow);

            this.Resources["Foreground"] = new SolidColorBrush(Colors.Black);
        }

Efekt?





Właściwie w tym przypadku możemy mówić o braku jakiegokolwiek efektu. Dzieje się tak dlatego, że statyczny zasób pobierany jest tylko raz. Obiekt pędzla został pobrany i nie zmienia się, jednak jeżeli zmienimy właściwość - zobaczymy efekt (dotyczy to pierwszego przykładu).

3. Przypisanie dynamiczne i zmiana koloru pędzla

Code:
<Button Background="{DynamicResource Background}" Foreground="{DynamicResource Foreground}" Content="Przycisk"></Button>

        private void BtnDoSomething_OnClick(object sender, RoutedEventArgs e)
        {
            ((SolidColorBrush) this.Resources["Background"]).Color = Colors.Yellow;

            ((SolidColorBrush)this.Resources["Foreground"]).Color = Colors.Black;
        }

Efekt:


4. Przypisanie dynamiczne i zmiana pędzla
Kod XAML przycisku taki jak poprzednio

Code:
        private void BtnDoSomething_OnClick(object sender, RoutedEventArgs e)
        {
            this.Resources["Background"] = new SolidColorBrush(Colors.Yellow);

            this.Resources["Foreground"] = new SolidColorBrush(Colors.Black);
        }

Efekt:


Warto zapamiętać te przypadki i wiedzieć w jaki sposób są pobierane zasoby, gdyż możemy dzięki temu uniknąć częstych błędów.