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.

sobota, 6 marca 2010

Praca z Menu w WPF

Każda nowoczesna aplikacja powinna mieć dobrze zaprojektowane i wygodne w obsłudze menu. W WPF bez trudu zaprojektujemy atrakcyjne menu, które nie tylko ułatwi poruszanie się użytkownikowi po naszej aplikacji, ale sprawi że będzie częściej sięgał po naszą aplikację.

Menu w wygodny sposób można zaprojektować z użyciem XAMLa.  

        <Menu>

        </Menu>

Pomiędzy znaczniki Menu możemy wstawiać kolejne elementy naszego menu:

        <Menu>
            <MenuItem Header="Plik" Name="file"></MenuItem>
            <MenuItem Header="Widok" Name="view"></MenuItem>
            <MenuItem Header="Pomoc" Name="help"></MenuItem>
        </Menu>

Użytkownikowi wyświetli się oczywiście to co zawarliśmy we właściwości Header. Aby dodać zdarzenie dla elementu menu, wystarczy wpisać Click=" a edytor sam zaproponuje stworzenie odpowiedniego EvntHandlera. 

Tworzenie menu wielopoziomowego także nie jest trudne. Wystarczy w konkretnym MenuItem-ie wprowadzić kolejny MenuItem:

        <Menu>
            <MenuItem Header="Plik" Name="file">
                <MenuItem Header="Otwórz" Name="open"></MenuItem>
                <MenuItem Header="Zapisz" Name="save"></MenuItem>
            </MenuItem>
            <MenuItem Header="Widok" Name="view"></MenuItem>
            <MenuItem Header="Pomoc" Name="help"></MenuItem>
        </Menu>

Jeśli chcielibyśmy uzyskać kolejny poziom dodajemy po prostu następny zagnieżdżony znacznik MenuItem. 

Często oprócz opisu tekstowego menu zawierają jeszcze pictogram tekstowego znaczenia. Ułatwia to wyszukiwanie konkretnych opcji w rozbudowanych menach a zarazem jest przyjemne dla oka. Aby dodać pictogram należy skorzystać z właściwości Icon:

        <Menu>
            <MenuItem Header="Plik" Name="file">
                <MenuItem Header="Otwórz" Name="open">
                    <MenuItem.Icon>
                        <Image Source="open.png"></Image>
                    </MenuItem.Icon>
                </MenuItem>
                <MenuItem Header="Zapisz" Name="save">
                    <MenuItem.Icon>
                        <Image Source="sae.png"></Image>
                    </MenuItem.Icon>
                </MenuItem>
            </MenuItem>
            <MenuItem Header="Widok" Name="view"></MenuItem>
            <MenuItem Header="Pomoc" Name="help"></MenuItem>
        </Menu>

Praca z Bitmapami w WPF

Dzisiaj trochę informacji, jak w WPF zabrać się za obróbkę zdjęć i ogólne aspekty związane z kontrolką Image.

1. Wczytywanie obrazka do kontrolki Image:
Zadanie to można wykonać na kilka sposobów: strumień, pobranie z żądanej lokalizacji sieciowej lub ścieżka. Najprostszą i najczęściej stosowaną opcją jest wczytanie ze ścieżki:


                        Image image = new Image();
                        BitmapImage bitmapImage = new BitmapImage(new Uri("c:\obraz.png"));
                        image.Source = bitmapImage;

2. Zapisywanie zawartości kontrolki Image do pliku.
Tutaj niestety bez pomocy strumienia się nie obejdzie. 
                SaveFileDialog saveFileDialog = new SaveFileDialog();
                saveFileDialog.Filter = "Pliki BMP | *.bmp | Pliki PNG | *.png";
                if (saveFileDialog.ShowDialog(this) == true)
                {
                    FileStream saveStream = new FileStream(saveFileDialog.FileName, FileMode.OpenOrCreate);
                    BmpBitmapEncoder encoder = new BmpBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(Image.Image));
                    encoder.Save(saveStream);
                    saveStream.Close();
                }

Kod trochę bardziej skomplikowany od tego znanego w WindowsForms, ale działa na takiej samej zasadzie :) Oczywiście jeżeli chcemy zapisać obrazek w innym formacie, możemy skorzystać z innego enkodera.

3. Dopasowanie rozmiarów obiektu Image do wczytywanego obrazka

                    Bitmap = new BitmapImage(new Uri(openFileDialog.FileName));

                    Image.Width = Bitmap.Width;
                    Image.Height = Bitmap.Height;

4. Konwersja bitmap
W WinForms istniała dziecinnie prosta możliwość zmiany wartości pojedyńczych pixeli na naszym obrazku.
Dla zobrazowania tego co chcemy zrobić przypuśćmy scenariusz w którym chcielibyśmy zmienić skalę kolorów na szare. W WinForms można by to zrobić w taki sposób:

            Stopwatch clock = new Stopwatch();
            clock.Start();
            Bitmap img = new Bitmap(Image.FromFile(@"c:\o.png"));
            int rows = img.Width;
            int cols = img.Height;
            Color c;
            int b;
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < cols; j++)
                {
                    c = img.GetPixel(i, j);
                    b = (c.R + c.G + c.B) / 3;
                    img.SetPixel(i, j, Color.FromArgb(b, b, b));
                }
            }
            pictureBox1.Image = img;
            clock.Stop();
            MessageBox.Show(clock.ElapsedMilliseconds.ToString());

Jak widać dołożyłem do kodu Stopwatch aby sprawdzić ile trwa konwersja do skali szarości w tym rozwiązaniu. Podczas konwersji wykorzystuje bitmapę o wielkości 512 x 512:

Jak widać średni czas konwersji takiego imaga zajmuje > 600 ms. Nie jest to zadowalający czas. Należy też wziąć pod uwagę fakt, że zdjęcia w większości przypadków są większej rozdzielczości. Nasz sposób sprawia, że wraz ze zwiększaniem rozdzielczości będzie rósł czas konwersji. Dzieje się tak z powodu powolnego działania funkcji GetPixel i SetPixel. Istnieje kilka rozwiązań tego problemu. Można skorzystać z kodu unsafe lub bardzo rozbudowanego mechanizmu oferowanego przez klasę ColorMatrix.

W WPF niestety (albo na szczęście) nie istnieją metody GetPixel i SetPixel.Aby zmienić wartość pojedynczego pixela należy użyć klasy WriteableBitmap, która posiada dwie metody: CopyPixels i WritePixels. Pierwsza z nich kopiuje wartości pixeli z Bitmapy do tablicy a druga nadpisuje wartości pixeli w Bitmapie.Zwrócić należy tutaj na słowo wartości pixeli. Otóż każdy pixel jest reprezentowany przez 3 składowe RGB i A (alpha - przeźroczystość). Czyli dla przykładu pierwszy pixel będzie zapisany na pierwszych 4 pozycjach naszej tablicy, drugi na kolejnych 4 itd.
Zobaczmy na przykład:

            BitmapImage image = new BitmapImage(new Uri(@"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"));
            width = (int)image.Width;
            height = (int)image.Height;
            _bitmap = new WriteableBitmap(image);
            stride = width * ((this._bitmap.Format.BitsPerPixel + 7) / 8);
            int arraySize = stride * height;
            byte[] pixels = new byte[arraySize];
            _bitmap.CopyPixels(pixels, stride, 0);
            int color = 0;
            int j = 0;
            for (int i = 0; i < pixels.Length / 4; ++i)
            {
                color = (pixels[j] + pixels[j + 1] + pixels[j + 2]) / 3;
                pixels[j] = (byte)color;
                pixels[j + 1] = (byte)color;
                pixels[j + 2] = (byte)color;
                pixels[j + 3] = 255;
                j += 4;
            }

            Int32Rect rect = new Int32Rect(0, 0, width, height);
            _bitmap.WritePixels(rect, pixels, stride, 0);
            Image.Source = _bitmap;

Kod może trochę bardziej rozbudowany od znanego z WinForms ale za to działający znacznie szybciej. O ile szybciej? O tym przekonamy się podczas tego testu:
Jak widać czas wynosi około 42 ms co jest dużo lepszym rezultatem. Kolejne czasy na poziomie 9 - 13 ms są wynikiem używania przez WPF Intelligent Redrawing (więcej o tym narzędziu można poczytać na stronach msdn).
 Jeżeli mowa o zamianie na skalę szarości nie można pominąć tego co sam framework oferuje. Dzięki klasie FormatConvertedBitmap mamy możliwość konwersji obrazka na dowolną skalę. Przykład:



            BitmapImage image = new BitmapImage(new Uri(@"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"));
            FormatConvertedBitmap format = new FormatConvertedBitmap();
            format.BeginInit();
            format.Source = image;
            format.DestinationFormat = PixelFormats.Gray32Float;
            format.EndInit();
            Image.Source = format;

Kod jak widać bardzo prosty, ciekawe jak z wydajnością?:

Wydajność całkiem niezła, czas konwersji zmniejszył się ponad 4 krotnie. A kolejne przemalowania nie zabierają praktycznie żadnego czasu (jedynie kiedy badamy takty procesora można wychwycić jakieś zmiany). 
Oczywiście naszą metodę można przyśpieszyć poprzez użycie wskaźników w trybie unsafe.

Memento Pattern

Często stajemy przed problemem, kiedy chcemy udostępnić użytkownikowi naszej aplikacji cofnięcie jakiejś decyzji. Największe znaczenie taka operacja pełni w grach komputerowych, gdzie zapisywane są tzw. checkpoints dzięki którym gracz jest w stanie cofnąć się do wcześniejszej fazy gry.
W swoim aktualnym projekcie, potrzebowałem stworzyć mechanizm umożliwiający cofnięcie zmian wprowadzonych w obrazie. Dzięki wzorcowi Memento można w bardzo łatwy i szybki sposób uzyskać taką funkcjonalność. 


Co nam daje Memento?
Pozwala na zachowanie całego stanu obiektu (lub tyle ile potrzebujemy), w celu późniejszego jego odtworzenia, bez naruszenia zasad enkapsulacji.
Spójrzmy na poniższy diagram klas:

Mamy tutaj 3 klasy:
Orginator - jest to obiekt którego stan wewnętrzny chcemy zachować. Poprzez metodę CreateMemennto() tworzy "kopię zapasową" samego siebie a dzięki metodzie SetMemento(Memento) może się odtworzyć z kopi. 

Memento - obiekt który jest kopią oryginalnego obiektu. Dzięki metodą GetState() i SetState() pozwalają na odczyt i zapis stanu obiektu.



Caretaker - obiekt ten zarządza obiektami Memento


Przykłady zawsze lepiej działają na wyobraźnię, dlatego spójrzmy na oto taki przykład. Mamy aplikację w której użytkownik wypełnia jakieś pola. Niech to będzie Imię, Nazwisko i wiek. Przyjmijmy, że wygląda ona tak:


Przyjmijmy, że po naciśnięciu przycisku dane zapisują się do bazy danych. Teraz nasz użytkownik wpisuje swoje dane, wciska ok i nagle przypomina mu się że źle wpisał nazwisko. Są dwa wyjścia: wpisze wszystko od początku albo kliknie przycisk cofnij i wpisze poprawne dane. W tym konkretnym przypadku nie wiele będzie kosztowała go pomyłka, jeżeli jednak pól by było więcej to możliwość cofnięcia wprowadzonych danych będzie z pewnością bardzo przydatna. Kod aplikacji:

    public class Person

    {

        private string _firstName;

        private string _lastName;

        private int _age;

 

        public string FirstName

        {

            get

            {

                return _firstName;

            }

            set

            {

                _firstName = value;

            }

        }

 

        public string LastName

        {

            get

            {

                return _lastName;

            }

            set

            {

                _lastName = value;

            }

        }

 

        public int Age

        {

            get

            {

                return _age;

            }

            set

            {

                _age = value;

            }

        }

 

        /// <summary>

        /// Przywraca poprzedni stan obiektu

        /// </summary>

        /// <param name="memento"></param>

        public void SetMemento(Memento memento)

        {

            this._age = memento.Age;

            this._firstName = memento.FirstName;

            this._lastName = memento.LastName;

        }

 

        /// <summary>

        /// Zapisuje aktualny stan obiektu

        /// </summary>

        /// <returns></returns>

        public Memento CreateMemento()

        {

            return new Memento(_firstName, _lastName, _age);

        }

    }


    public class Memento

    {

        private string _firstName;

        private string _lastName;

        private int _age;

 

        public string FirstName

        {

            get

            {

                return _firstName;

            }

            set

            {

                _firstName = value;

            }

        }

 

        public string LastName

        {

            get

            {

                return _lastName;

            }

            set

            {

                _lastName = value;

            }

        }

 

        public int Age

        {

            get

            {

                return _age;

            }

            set

            {

                _age = value;

            }

        }

 

        public Memento(string firstName, string lastName, int age)

        {

            _firstName = firstName;

            _lastName = lastName;

            _age = age;

        }

 

 

    }


    public class Caretaker

    {

        private Memento _memento;

        public Memento Memento

        {

            get

            {

                return _memento;

            }

            set

            {

                _memento = value;

            }

        }

    }



    public partial class Form1 : Form
    {
        Person person;
        Caretaker[] caretaker;
        int counter = 0;
        public Form1()
        {
            InitializeComponent();
            person = new Person();
            caretaker = new Caretaker[10];
        }

        private void DisplayPersonInformation()
        {
            textBox1.Text = person.FirstName;
            textBox2.Text = person.LastName;
            textBox3.Text = person.Age.ToString();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (counter<10)
            {
                caretaker[counter] = new Caretaker();
                caretaker[counter].Memento = person.CreateMemento();
                counter++;
                person.FirstName = textBox1.Text;
                person.LastName = textBox2.Text;
                person.Age = int.Parse(textBox3.Text);
            }

        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (counter>1)
            {
                counter--;
                person.SetMemento(caretaker[counter].Memento);
                DisplayPersonInformation();
            }
        }
    }

Przykład być może nie jest najwyższych lotów, jednak w prosty sposób obrazuje możliwość wykorzystania tego wzorca w swoich projektach. Jak pisałem wcześniej w swoim projekcie potrzebowałem możliwości cofania zmian wprowadzanych na obrazie. Dla takich zastosowań memento nadaje się idealnie :)