niedziela, 28 marca 2010

Singleton Pattern

Kolejnym wzorcem projektowym, który omówię jest Singleton. Jest to jeden z najprostszych wzorców projektowych, ale i jedyny który ma tak wiele różnych implementacji.

Wzorzec ten pozwala na tworzenie tylko jednego obiektu. Spytasz po co mi tylko jeden obiekt danej klasy? Przydaje się to w wielu miejscach aplikacji. Przykładowo kilka zastosowań to: ustawienia aplikacji, dostęp do pliku (zapis), tworzenie logów.
Zapewne może się wydawać, że tworzenie specjalnego wzorca dla tego typu zastosować jest nieopłacalne. Ktoś może powiedzieć: skoro mamy utworzyć jeden obiekt to zróbmy jedną zmienną globalną i bez cyrków mamy to co nam potrzeba. Jest jednak jeden minus tego rozwiązania. Jeśli tworzymy zmienną globalną to utworzyć musimy ją przy starcie naszej aplikacji a jeśli nasza aplikacja działa bardzo długo to tym gorzej dla nas - marnujemy cenne zasoby. Singleton pozwala na utworzenie siebie tylko wtedy kiedy jest na prawdę potrzebny.

Spójrzmy na diagram UML:
Jak widać mamy tu do czynienia z jedną klasą Singleton. Konstruktor klasy jest prywatny, dzięki czemu zapobiegamy tworzeniu obiektów tej klasy. Pole instance przechowuje utworzony obiekt Singletonu a metoda GetInstance tworzy lub zwraca już istniejący obiekt Singletona.

Zobaczmy na przykładową implementację tego wzorca:
    1     class Singleton
    2     {
    3         private static Singleton _instance;
    4         public int i;
    5         private Singleton()
    6         {
    7         }
    8 
    9         public static Singleton GetInstance()
   10         {
   11             return _instance == null ? (_instance = new Singleton()) : _instance;
   12         }
   13     }
   14 
   15     class Program
   16     {
   17         static void Main(string[] args)
   18         {
   19             Singleton sing = Singleton.GetInstance();
   20             sing.i = 20;
   21             Console.WriteLine(sing.i);
   22             Singleton s = Singleton.GetInstance();
   23             Console.WriteLine(sing.i);
   24         }
   25     }

Implementacja wydaje mi się bardzo prosta w zrozumieniu. W klasie mamy statyczne pole _instance i metodę GetInstance(), która w zależności od tego czy jeszcze nie utworzono obiektu czy też utworzono zwraca odpowiednią wartość.

Kod przedstawiony powyżej, pomimo swojej prostoty, może sprawić nam nie lada problem w aplikacjach wielowątkowych. Podczas gdy nasz kod wykorzystywałby wiele wątków, mogło by dojść do sytuacji kiedy każdy wątek w tej samej chwili spróbowałby utworzyć instancję Singletonu - a wtedy katastrofa murowana. Najprostszym rozwiązaniem jest zastosowanie tworzenia obiektu w deklaracji:

    1 class Singleton
    2     {
    3         private static readonly Singleton _instance = new Singleton();
    4         public int i;
    5         private Singleton()
    6         {
    7         }
    8 
    9         public static Singleton GetInstance()
   10         {
   11             return _instance;
   12         }
   13     }

Framework .NET gwarantuje w takim przypadku, że pole jest thread safe. W dokumentacji na MSDN można znaleźć wiele takich adnotacji do istniejących klas w .NET.
Inną opcją jest tzw. podwójne sprawdzanie. Jednak podana wyżej metoda jest prostsza i szybsza w implementacji.

Wiele innych metod implementacji Singletonu można znaleźć na wielu internetowych stronach. Polecam zaznajomić się z innymi możliwymi konstrukcjami.

Abstract Factory Pattern

W kolejnym poście poświęconym wzorcom projektowym, zajmiemy się rozszerzeniem wzorca Factory Method. W definicji tego wzorca możemy przeczytać: Abstract Factory udostępnia interfejs pozwalający na tworzenie rodzin produktów. Spójrzmy na diagram UML:

Interfejs AbstractFactory definiuje interfejs, który musi być implementowany przez konkretne fabryki tworzące produkty (obiekty).
Concrete Factories odpowiadają za utworzenie konkretnych produktów.
Abstract Product definiuje interfejs rodziny produktów.
Product - produkt żądany przez klienta.

Przejdźmy do przykładu:
Przyjmijmy że mamy dwie piekarnie. Jedna zlokalizowana jest w Krakowie druga w Warszawie. Obie pieką placki kruche i z owocami. Piekarnia w Krakowie piecze placek z jabłkami i śliwkami. Piekarnia w Warszawie piecze placki z wiśniami i truskawkami. Przyjrzyjmy się temu opisowi od strony projektanta oprogramowania. Klient który zamawia placki ma możliwość zamówienia placka od dwóch różnych dostawców (piekarnie Kraków i Warszawa). W każdej z piekarń produkuje się placki o różnych składach a to co je łączy, to to, że są to placki typu Kruchy i Owocowy. Te informacje są wystarczające do zbudowania wzorca Abstract Factory. (Dodałem jeszcze pole price zawierające cenę placka - nie używałem go jednak; można samemu dodać np. wyświetlanie ceny po wywołaniu metody opis).
A więc przejdźmy do sedna czyli kodu :) :

    1     public abstract class Bakery
    2     {
    3         public abstract IFruitPie CreateFruitPie();
    4         public abstract ITartPie CreateTartPie();
    5     }
    6 
    7     public class KrakowBakery : Bakery
    8     {
    9         public override IFruitPie CreateFruitPie()
   10         {
   11             return new ApplePie(23.50);
   12         }
   13 
   14         public override ITartPie CreateTartPie()
   15         {
   16             return new PlumPie(28.40);
   17         }
   18     }
   19 
   20     public class WarsawBakery : Bakery
   21     {
   22         public override IFruitPie CreateFruitPie()
   23         {
   24             return new CherryPie(33.68);
   25         }
   26 
   27         public override ITartPie CreateTartPie()
   28         {
   29             return new StrawberryPie(39.40);
   30         }
   31     }
   32 
   33     public interface IFruitPie
   34     {
   35         string Description();
   36         double Price { get; }
   37     }
   38 
   39     public interface ITartPie
   40     {
   41         string Description();
   42         double Price { get; }
   43     }
   44 
   45     public class PlumPie : ITartPie
   46     {
   47         private double _price;
   48 
   49         public PlumPie(double price)
   50         {
   51             this._price = price;
   52         }
   53         #region ITartPie Members
   54         public string Description()
   55         {
   56             return "Tart Plum Pie";
   57         }
   58 
   59         public double Price
   60         {
   61             get { return this._price; }
   62         }
   63 
   64         #endregion
   65     }
   66 
   67     public class StrawberryPie : ITartPie
   68     {
   69         private double _price;
   70 
   71         public StrawberryPie(double price)
   72         {
   73             this._price = price;
   74         }
   75         #region ITartPie Members
   76         public string Description()
   77         {
   78             return "Tart Strawberry Pie";
   79         }
   80 
   81         public double Price
   82         {
   83             get { return this._price; }
   84         }
   85 
   86         #endregion
   87     }
   88 
   89     public class ApplePie : IFruitPie
   90     {
   91         private double _price;
   92 
   93         public ApplePie(double price)
   94         {
   95             this._price = price;
   96         }
   97         #region IFruitPie Members
   98 
   99         public string Description()
  100         {
  101             return "Pie with fresh Apples";
  102         }
  103 
  104         public double Price
  105         {
  106             get { return _price; }
  107         }
  108 
  109         #endregion
  110     }
  111 
  112     public class CherryPie : IFruitPie
  113     {
  114         private double _price;
  115 
  116         public CherryPie(double price)
  117         {
  118             this._price = price;
  119         }
  120         #region IFruitPie Members
  121 
  122         public string Description()
  123         {
  124             return "Cherry Pie with fresh Cherries";
  125         }
  126 
  127         public double Price
  128         {
  129             get { return _price; }
  130         }
  131 
  132         #endregion
  133     }
  134 
  135     class Program
  136     {
  137         static void Main(string[] args)
  138         {
  139             Bakery bakery = new KrakowBakery();
  140             IFruitPie FruitPie = bakery.CreateFruitPie();
  141             Console.WriteLine(FruitPie.Description());
  142             ITartPie TartPie = bakery.CreateTartPie();
  143             Console.WriteLine(TartPie.Description());
  144 
  145             Console.WriteLine("---------");
  146 
  147             bakery = new WarsawBakery();
  148             FruitPie = bakery.CreateFruitPie();
  149             Console.WriteLine(FruitPie.Description());
  150             TartPie = bakery.CreateTartPie();
  151             Console.WriteLine(TartPie.Description());
  152         }
  153     }

Może kilka słów o implementacji.
1 - 5 - definicja abstrakcyjnej klasy Abstract Factory
7 - 31 - definicje konkretnych fabryk
33 - 43 - interfejsy odpowiedzialne za typy produktów
44 - 132 - definicje konkretnych produktów (placków)

Powyższa implementacja może zostać ulepszona poprzez np. użycie typów generycznych - dzięki temu tworzymy wzorzec do generowania konkretnych fabryk czy produktów (mniej pisania).

Kiedy stosować Abstract Factory?
- generowanie widoku i interfejsu aplikacji;
- interakcja z systemami operacyjnymi (w każdym mamy takie operacje jak otwórz, zapisz, wyświetl - tyle, że w Windowsie jest to inaczej realizowane niż np w Linuxie);
- kiedy znamy cały zestaw tworzonych produktów (dodanie nowego wymaga zmiany we wszystkich fabrykach)

Co zyskujemy dzięki Abstract Factory?
- łatwa zmiana całych "rodzin produktów" (np. zmiana skórek w aplikacji)
- ukrycie implementacji przed klientem
- logiczna spójność systemu - fabryka odpowiada za tworzenie odpowiednich produktów

sobota, 20 marca 2010

Tworzenie kontrolek w ASP.NET

ASP.NET pozwala na tworzenie kontrolek, które pozwalają na re użycie kodu pojawiającego się w wielu miejscach tworzonego projektu, czy nawet wielu projektów.

Najprostszym typem kontrolek są kontrolki bazujące na istniejących już kontrolkach. Najłatwiej to opisać jako wrzucanie na formatkę różnych komponentów (labeli, textboxów, buttonów itp.), które następnie w projekcie można przeciągnąć jako pojedynczą kontrolkę. Padnie pytanie: co nam to daje, jeżeli i tak przeciągamy komponenty z toolboxa na formatkę? Daje nam to, że jeżeli w wielu częściach witryny mamy ten sam formularz (np. do wprowadzania adresu lub jakichś spersonalizowanych informacji) nie musimy za każdym razem przeciągać 50 textboxów i buttonów. Dodatkowo możemy w naszej kontrolce zaimplementować potrzebną walidację.

Dla przykładu stworzymy kontrolkę oferującą wprowadzenie adresu.
W VS 2010 (2008 wygląda to tak samo), wybieramy prawym klawiszem nasz projekt i z menu kontekstowego wybieramy Add->Add New Item... Następnie Web User Control, nadajemy jej jakąś nazwę i klikamy na Ok. Do projektu zostanie dodany plik ascx, który następnie pozwala na "przeciągnięcie się do naszej właściwej strony. Niestety, ten rodzaj kontrolki nie pozwala na umieszczenie się w toolbox-ie. A korzystanie ogranicza się do aktualnego projektu.

Kod, który pozwala uzyskać taki wygląd:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Kontrolka.ascx.cs" Inherits="WebApplication1.Kontrolka" %>
<table>
    <tbody>
        <tr>
            <td>Miasto: </td>
            <td><asp:TextBox ID="tbCity" runat="server"></asp:TextBox></td>
        </tr>
        <tr>
            <td>Ulica: </td>
            <td><asp:TextBox ID="tbStreet" runat="server"></asp:TextBox> </td>
        </tr>
        <tr>
            <td>Nr domu: </td>
            <td><asp:TextBox ID="tbStreetNumber" runat="server"></asp:TextBox></td>
        </tr>
        <tr>
            <td>Kod pocztowy: </td>
            <td><asp:TextBox ID="tbPostalCode1" runat="server" MaxLength="2" Width="32px"></asp:TextBox><asp:TextBox
                    ID="tbPostalCode2" runat="server" MaxLength="3" Width="50px"></asp:TextBox></td>
        </tr>
        <tr>
            <td><asp:Button ID="bOk" runat="server" Text="Ok" /></td>
        </tr>
    </tbody>
</table>

Przejdźmy dalej, w naszej aplikacji chcemy aby po kliknięciu w przycisk dodawać np. do listy czy też bazy danych nowe informacje (albo wyszukiwać bazie, w każdym razie zastosować można znaleźć tysiące :)). Osiągnąć możemy to poprzez dodanie zdarzenia, wywoływanego w momencie naciśnięcia przycisku Ok. Zdarzenie tworzy się tak samo jak dla aplikacji WinForms:
1. Tworzymy klasę dziedziczącą po EventArgs:

    public class AdressEventArgs : EventArgs
    {
        public string City { get; private set; }
        public string Street { get; private set; }
        public int StreetNumber { get; private set; }
        public string PostalCode { get; private set; }

        public AdressEventArgs(string city, string street, int streetNumber, string postalCode)
        {
            this.City = city;
            this.PostalCode = postalCode;
            this.Street = street;
            this.StreetNumber = streetNumber;
        }
    }

2. Tworzymy delegatę:

        public delegate void OkButtonClickHandler(object sender, AdressEventArgs e);

3. Tworzymy Event, którego typem jest nasza delegata:

        public event OkButtonClickHandler OkButtonClick;

4. W metodzie kliknięcia przycisku obsługujemy nasz event:

        protected void bOk_Click(object sender, EventArgs e)
        {
            if (OkButtonClick != null)
            {
                OkButtonClick(this, new AdressEventArgs(tbCity.Text, tbStreet.Text, int.Parse(tbStreetNumber.Text), this.tbPostalCode1.Text + "-" + this.tbPostalCode2.Text));
            }
        }

Oczywiście pominąłem tu sprawę walidacji pól.
 Teraz zajmiemy się dodaniem tak stworzonej kontrolki do naszego projektu. Wchodzimy na tworzoną przez nas stronę i przeciągamy z Solution Explorer naszą kontrolkę. Aby obsłużyć nasze zdarzenie dodajemy do metody Page_Init następujący kod:

        protected void Page_Init(object sender, EventArgs e)
        {
            Kontrolka1.OkButtonClick += new Kontrolka.OkButtonClickHandler(Kontrolka1_OkButtonClick);
        }

Oraz w metodzie która służy jako parametr zdarzenia:

        void Kontrolka1_OkButtonClick(object sender, AdressEventArgs e)
        {
            Response.Write(string.Format("{0} {1} {2} {3}", e.City, e.PostalCode, e.Street, e.StreetNumber));
        }

W moim przypadku drukuję na stronie dane które wpisał użytkownik do naszej kontrolki, ale równie dobrze można otrzymane dane wprowadzić do bazy czy też użyć jako parametry do wyszukiwania na mapie itp.

Kilka dodatkowych operacji które mogą nam się przydać podczas pisania własnych kontrolek:
1. Dodawanie dodatkowych właściwości (dostępnych poprzez IntelliSense) pozwalających na np. ustawienie długości textboxa.
Jest to bardzo proste do uzyskania. W klasie kontrolki po prostu dodajemy kolejne właściwości z odpowiednimi atrybutami:



        private int _maxCityLength;

        public int MaxCityLength
        {
            get
            {
                return _maxCityLength;
            }
            set
            {
                _maxCityLength = value;
                tbCity.MaxLength = 5;
            }
        }

2. Dostęp do kontrolek zawartych w User Control.
Ponieważ dodawane kontrolki (np textboxy) w User Control mają atrybut private, nie mamy do nich dostępu z zewnątrz. Najprostszym rozwiązaniem byłoby uczynienie tych elementów publicznymi. Jednak należy tego unikać, gdyż narusza to zasady enkapsulacji. Lepiej wyodrębnić potrzebne nam elementy do właściwości. Jeżeli chcemy np. mieć dostęp do tekstu zawartego w polu city możemy to zrobić np. tak:

        public string CityText
        {
            get
            {
                return tbCity.Text;
            }
            set
            {
                tbCity.Text = value;
            }
        }

piątek, 19 marca 2010

Pliki BLOB w MS SQL

Przechowywanie plików binarnych w bazie danych nie jest spotyka często. Przeważnie tworzy się pole służące do adresu pod którym można znaleźć żądany plik a sam plik jest umieszczany na dysku poza bazą. Główną przyczyną takiego faktu jest szybko rosnący rozmiar bazy danych. Aby przechować jakikolwiek plik w bazie danych np. grafikę czy plik excela, należy ten plik przekształcić do postaci binarnej i w takiej postaci zapisać. Przy odczycie z bazy danych odczytujemy postać binarną i zapisujemy ją jako żądany plik.

Przykładowo spróbujmy do bazy danych zapisać plik tekstowy. Baza danych składa się z jednej tabeli. Polami w tej tabeli są:
Plik tekstowy zawiera jedno zdanie: "Ala ma kota". Warto zauważyć że wartość pola varbinary można ustawić także na wartość max co pozwoli nam na przechowywanie plików do 2GB.

Kod pozwalający na zapis do bazy danych pliku:

        private void button1_Click(object sender, EventArgs e)
        {
            byte[] file;
            using (FileStream fs = new FileStream(@"d:\plik.txt", FileMode.Open, FileAccess.Read))
            {
                file = new byte[fs.Length];
                fs.Read(file, 0, (int)fs.Length);
            }
            using (SqlConnection conn = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=test;Integrated Security=True"))
            {
                conn.Open();
                using (SqlCommand cmd = new SqlCommand("INSERT INTO t1(data) VALUES(@binaryData)", conn))
                {
                    cmd.Parameters.Add(new SqlParameter("@binaryData", file));
                    cmd.ExecuteNonQuery();
                }
            }
        }

Odczyt z bazy danych jest bardzo podobny. Zapisując pole jako ciąg bajtów musimy je też w taki sam sposób odczytać:

        private void button2_Click(object sender, EventArgs e)
        {
            byte[] file;
            using (SqlConnection conn = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=test;Integrated Security=True"))
            {
                conn.Open();
                using (SqlCommand cmd = new SqlCommand("SELECT TOP 1 data FROM t1", conn))
                {
                    file = (byte[])cmd.ExecuteScalar();
                }
            }
            using (FileStream fs = new FileStream(@"d:\kopia.txt", FileMode.Create, FileAccess.Write))
            {
                fs.Write(file, 0, file.Length);
            }
        }

poniedziałek, 15 marca 2010

LINQ to XML

Szukając informacji na temat LINQ można trafić na bardzo wiele przykładów odnoszących się do LINQ to SQL i LINQ to object, ale bardzo ciężko trafić na solidną literaturę odnośnie dostępu do plików XML. Zastanawia mnie tylko dlaczego jest taka sytuacja, przecież XML jest jednym z najpopularniejszych formatów przechowywania danych (po bazie danych oczywiście).

Nie wgłębiając się w filozoficzne zawiłości tego tematu spróbuje w prosty sposób opisać możliwości jakie daje LINQu podczas pracy z plikiem XML.

 1. Utworzenie pliku XML
Zacznijmy od podstaw czyli od tego w jaki sposób utworzyć prosty plik XML. W tym miejscu mam nadzieję, że czytelnik zna strukturę i budowę plików XML. Jeżeli nie polecam zapoznać się z materiałem zawartym np na stronie http://www.w3schools.com/xml/default.asp.
Przejdźmy więc do utworzenia prostego dokumentu XML:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            XDocument documentXML = new XDocument(
                new XDeclaration("1.0", "utf-8", "yes"),
                new XElement("Books",
                    new XElement("Book",
                        new XAttribute("Id", 0),
                        new XElement("Title", "Inżynieria oprogramowania"),
                        new XElement("Author", "Ian Sommerville"),
                        new XElement("Pages", 685),
                        new XElement("Publisher", "WNT"))));
            //Printing creating document
            Console.WriteLine(documentXML);
        }
    }
}

Jest to nowy sposób tworzenia dokumentów XML. Kiedyś stosowany DOM jest o wiele bardziej nieczytelny od zaprezentowanego powyżej. Trochę objaśnień do kodu. Za pomocą atrybutu XDocument definiujemy nowy dokument XML. Następnie tworzymy go i w konstruktorze tworzymy deklarację dokumentu XML. Kolejne zagłębienia definiują kolejno atrybuty oraz elementy dokumentu. Należy tutaj zwrócić uwagę na stawianie nawiasów, gdyż ma to kluczowe znaczenie podczas tworzenia drzewa dokumentu. Na końcu wyświetlamy zawartość naszego dokumentu.

2. Zapis pliku XML na dysk
Zapis na dysk jest bardzo prosty i wymaga wywołania jednej metody Save:

            //...
            documentXML.Save(@"d:\plik.xml");

3. Odczyt pliku XML z dysku
Odczyt, podobnie jak zapis jest niezwykle prosty:
  
            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            Console.WriteLine(loadFromDisk);

4. Dodawanie nowych elementów
Aby dodać nowy element do naszego dokumentu:
1. Pobieramy obiekt do którego chcemy dodać dane
2. Używając metody Add() dodajemy nowe wartośći
Przykład:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");

            XElement root = loadFromDisk.Root;
            root.Add(new XElement("Book",
                new XAttribute("Id", 1),
                new XElement("Title", "Podstawy programowania współbierznego i rozproszonego"),
                new XElement("Author", "Mordechai Ben-Ari"),
                new XElement("Pages", 332),
                new XElement("Publisher", "WNT")));

            Console.WriteLine(loadFromDisk);

Należy pamiętać, że zmiany są zapisywane w pamięci komputera a nie na dysku. Jeżeli chcemy aby dane zapisały się na dysku twardym należy wywołać metodę Save().

5. Przechodzenie po drzewie dokumentu
Przechodzenie po dokumencie jest niezwykle proste. Wystarczy zwykła pętla aby wyświetlić wszystkie interesujące nas atrybuty czy elementy. Kilka przykładów wraz z komentarzem dla utworzonego wcześniej dokumentu:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");

            //1. Wyświetlenie wszystkich elementów w drzewie
            foreach (var item in loadFromDisk.Elements())
            {
                Console.WriteLine(item);
            }
            Console.WriteLine("-------------------------------------------------");
            //2. To samo co wyżej ale ze specyfikacją głównej gałęzi
            foreach (var item in loadFromDisk.Elements("Books"))
            {
                Console.WriteLine(item);
            }
            Console.WriteLine("-------------------------------------------------");
            //3. Wyświetlenie informacji o samych książkach
            foreach (var item in loadFromDisk.Elements("Books").Elements("Book"))
            {
                Console.WriteLine(item);
            }
            Console.WriteLine("-------------------------------------------------");
            //4. Wyświetlenie tytułów książek
            foreach (var item in loadFromDisk.Elements("Books").Elements("Book").Elements("Title"))
            {
                Console.WriteLine(item);
            }
            Console.WriteLine("-------------------------------------------------");
            //5. Wyświtlenie Id książek
            foreach (var item in loadFromDisk.Elements("Books").Elements("Book").Attributes("Id"))
            {
                Console.WriteLine(item);
            }

6. Wyszukiwanie elementów na podstawie parametrów
Wyszukiwanie w dokumencie nie jest skomplikowane. Jeżeli przykładowo szukamy książki której Id = 1, możemy to zrobić w ten sposób:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            var query = from a in loadFromDisk.Root.Elements("Book")
                        where int.Parse(a.Attribute("Id").Value) == 1
                        select a;
            foreach (var item in query)
            {
                Console.WriteLine(item);
            }

Możemy także ograniczyć się do wyświetlenia tylko tytułów:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            var query = from a in loadFromDisk.Root.Elements("Book")
                        where int.Parse(a.Attribute("Id").Value) == 1
                        select a;
            foreach (var item in query)
            {
                Console.WriteLine(item.Element("Title"));
            }

Można także skorzystać z dobrodziejstwa typów anonimowych i ułatwić sobie wyświetlanie wybranych elementów:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            var query = from a in loadFromDisk.Root.Elements("Book")
                        where int.Parse(a.Attribute("Id").Value) == 1
                        select new
                        {
                            Id = int.Parse(a.Attribute("Id").Value),
                            Title = a.Element("Title").Value,
                            Author = a.Element("Author").Value,
                            Pages = int.Parse(a.Element("Pages").Value),
                            Publisher = a.Element("Publisher").Value
                        };
            foreach (var item in query)
            {
                Console.WriteLine(item.Id);
                Console.WriteLine(item.Title);
            }

Dzięki takiemu podejściu IntelliSense podpowie jakie parametry możemy wykorzystać. 

7. Uaktualnianie pliku XML
Uaktualnienie pliku można podzielić na 
a) uaktualnienie całego elementu 
b) uaktualnienie pod elementu

W pierwszym przypadku należy wyodrębnić element który chcemy uaktualnić. Możemy tu skorzystać z wielu dostępnych metod które oferuje klasa XElement:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            var query = from a in loadFromDisk.Root.Elements("Book")
                         where int.Parse(a.Attribute("Id").Value) == 1
                         select a;

            foreach (var item in query)
            {
                item.ReplaceAll(new XAttribute("Id", 10),
                    new XElement("Title", "Robinson Crusoe"),
                    new XElement("Author", "Daniel Defoe"),
                    new XElement("Pages", 242),
                    new XElement("Publisher", "Wordsworth classics"));
            }

            Console.WriteLine(loadFromDisk);

Jak widać uaktualnienie całego elementu nie wymaga wielkiej pracy. Jeszcze łatwiej jest z modyfikowaniem pojedynczej wartości. Dla przykładu zmienimy autora książki o Id = 0:

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            var query = from a in loadFromDisk.Root.Elements("Book")
                         where int.Parse(a.Attribute("Id").Value) == 0
                         select a;

            foreach (var item in query)
            {
                item.Element("Author").Value = "John Doe";
                //item.SetElementValue("Author", "John Doe");
                //item.Element("Author").SetValue("John Doe");
            }

W komentarzach podałem alternatywne metody wykonania tego samego zadania. Należy pamiętać, że dane nie zostaną zapisane na dysk do pliku XML bez jawnego wywołania metody Save()!

8. Usuwanie elementów
Usuwanie elementów przebiega tak samo jak uaktualnianie czy też modyfikowanie. Wyszukujemy interesujący nas element a następnie wywołujemy metodę Remove():

            XDocument loadFromDisk = XDocument.Load(@"d:\plik.xml");
            var query = from a in loadFromDisk.Root.Elements("Book")
                        where int.Parse(a.Attribute("Id").Value) == 0
                        select a;

            foreach (var item in query)
            {
                item.Remove();
            }

Mam nadzieję, że dzięki tym przykładom ułatwię trochę pracę z plikami XML przy użyciu technologi LINQ.