Windows Phone 7

Tydzień temu (oraz jakiś czas temu) miałem przyjemność co nie co opowiedzieć o nowym systemie na urządzenia mobilne Microsoftu – Windows Phone 7. Ba nawet udało się wypożyczyć jeden telefon z Microsoftu – LG GW910, który widać na rysunku poniżej:

image

Przy okazji pożyczenia korzystam sobie z niego od dwóch tygodni i muszę przyznać, że nowy system i sam telefon spisuje się bardzo dobrze, jestem pozytywnie zaskoczony działanie. Nie sądziłem, że system na telefonie może tak szybko działa, przy tym tak stabilnie (jak na razie nie miałem żadnych problemów). No ale nie ma co się dziwić, jednak pod maską siedzi mały potwór jak na urządzenia mobilne (ostatnio dla wujka oddałem swój stary komputer, aby mógł sobie korzystać z Internetu, który ma parametry zbliżone do parametrów tego telefonu Open-mouthed smile ).

No dobra ale wracając do mojej prezentacji (a dokładnie dwóch Smile ). Ogólnie była podzielona na trzy części.

Podczas pierwszej opowiedziałem o samym telefonie i systemie, co pojawiło się nowego i dlaczego. Nie będę tutaj tego wszystkiego opisywał szczególnie, że w innych miejscach w Internecie można znaleźć opisy ale wspomnę o jednej rzeczy. Osobiście bardzo podoba mi się nowy interfejs użytkownika (tak zwane Metro wzorowane na istniejących metrach – kolejkach podziemnych Smile ) zaproponowany przez Microsoft. Pamiętam kiedyś (bodajże na prezentacji z MIXa, gdzie po raz pierwszy został pokazany Windows Phone 7) jak na filmie widziałem nowy interfejs miałem co do niego mieszane uczucia, czy będzie fajny. Ale wystarczyło trochę poużywać nowy interfejs i bardzo przypadł mi do gustu. Bardzo szybko można się odnaleźć w nim i po chwili z nim bez problemów pracować.

Przy interfejsie też widać, że jednak Microsoft sporo czasu poświęcił na myślenie (nie wiem, czy gdzieś indziej ktoś na to wpadł, więc mogę się myli) ale zastanawialiście się, czemu domyślny interfejs Windows Phone 7 jest czarny? Okazuje się, że nie bez powodu. W telefonach z Windows Phone 7 wykorzystywane są wyświetlacze OLED, które jak “świecą” na czarno zużywają dużo mniej energii niż świecąc na jasno. W materiałach o Windows Phone 7 podaje się, że gdy właśnie ekran OLED “świeci” na czarno to zużywa połowę mniej energii niż identyczny ekran LCD, czyli jak widać znacznie mniej. Natomiast w przypadku świecenia jasno, tutaj ekrany OLED wypadają dużo gorzej niż ekran LCD – zużywa “tylko” trzy razy więcej energii niż ekran LCD. Więc widać to, że interfejs domyślnie jest ciemny to nie tylko widzi misie kogoś tam ale zamierzony cel, dzięki któremu telefon może działać dłużej na baterii.

Kolejną częścią prezentacji był Marketplace ale nie od strony zwykłego użytkownika ale od strony potencjalnego twórcy oprogramowania. Tutaj wspomnę tylko o dwóch rzeczach. Po pierwsze warto pisać aplikacje na Windows Phone 7, teraz jest ich bardzo mało w porównaniu np. z innymi systemami. Nie na ile w tej chwili te dane są aktualne ale z tego co się orientuje to w marketplace dla Windows Phone 7 aplikacji jest około 1500 tysiąca, natomiast w przypadku Androida jakiś czas temu widziałem informacje, że pękła liczba 100 tysięcy, czyli jak widać jest co nadgonić i co fajne od razu wrzucając aplikację do marketplace dystrybuowana jest ona globalnie.

Dodatkowo studenci aplikacje mogą wrzucać za darmo wystarczy mieć tylko konto w Dreamsparku, a takie konto może mieć każdy student. Niestety na razie tak różowo nie jest i jakimś cudem polscy studenci z tego nie mogą korzystać. Ale z tego co wiem to polski odział Microsoft pracuje nad tym, aby jednak dać możliwość studentom z Polski wrzucania aplikacji za darmo do marketplace. Jak coś więcej będę wiedział to na pewno dam znać Smile

Ostatnią częścią prezentacji był już sam Silverlight dla Windows Phone 7. Pokazałem kilka prostych dem, jak korzystać z tego co daje nam telefon i jak łatwo niektóre z nich osiągnąć. Nie będę tutaj opisywał dem (ale planuje to zrobić za jakiś czas na swoim blogu – http://plawgo.pl). Jak ktoś byłby zainteresowany już teraz jak pisać aplikacje to mogę polecić dwa miejsca w sieci:

Dla wszystkich, którzy chcą tworzyć aplikacje dla Windows Phone 7 życzę powodzenia!!!

Tagi:

Hasło - aplikacja MVVM

Na prezentacji pokazałem sposób tworzenia aplikacji WPF z wykorzystaniem Visual Studio, Blend i wzorca projektowego MVVM. Kod i slajdy będą na SkyDrive grupy. Dzisiaj chcę wam pokazać jak można wykonać podobną aplikacje która może nam posłużyć do „wymyślania” haseł na konta ;p
To do dzieła.
1 Tworzymy nowy projekt.
Nowy projekt utworzymy z wykorzystaniem Blenda. Jeżeli ktoś nie ma Blenda może śmiało to samo wykonać w Visual Studio. Uruchamiamy Blenda po czym w okienku które nam wyskoczyło naciskamy New Project. Alternatywnie możemy zrobić to samo przez File-> New Project. Wybieramy typ projektu jako WPF Application. Nazywamy nasz projekt Haslo, zapamiętujemy gdzie go zapisujemy i klikamy OK.
2 Projektujemy widok
Powinno ukazać się nam okno domyślnie utworzone przez Blenda. Będziemy dążyć aby wygląd naszej aplikacji był taki jak niżej.
Tworzyć nasz widok możemy na dwa sposoby, poprzez przeciąganie kontrolek(niektórzy mogą preferować ten sposób) lub przez deklaracje bezpośrednio w kodzie. Ja osobiście wolę ta druga metodę wiec jej będziemy używać. Aby zobaczyć nasz kod klikamy na View-> Active Document View-> Split View ( będziemy widzieć kod i wygląd naszej aplikacji ).  Zabieramy się za tworzenie widoku.  Zaczynamy od zdefiniowania w naszym Gridzie dwóch wierszy do których będziemy przypisywać kontrolki. Uzyskujemy to przez następujący kod:
<Grid.RowDefinitions>
           <RowDefinition Height="Auto"/>
           <RowDefinition Height="auto"/>
</Grid.RowDefinitions>
 Następnie dodajemy dwa StackPanele. Jeden przypisujemy do pierwszego wiersza, drugi do drugiego.
W pierwszym tworzymy Labela który będzie nam wyświetlać losowane liczby. Wielkość czcionki ustawiamy na 40, wyśrodkowujemy w pionie i poziomie. Jego zawartość(Content) musimy połączyć z właściwością ViewModel który będziemy tworzyć w dalszej części. Drugi StackPanel będzie troche bardziej złożony.  W jego wnętrzu deklarujemy dwa kolejne StackPanele. W obu ustawiamy orientację na poziomą i wyrównujemy w poziomie na środek. Do tej pory nasz fragment powinien wyglądać następująco.

 <StackPanel Grid.Row="0">
      <Label Content="{Binding Path=Liczby,UpdateSourceTrigger=PropertyChanged}" FontSize="40" HorizontalContentAlignment="Center"
                  VerticalContentAlignment
="Center"/>
      </StackPanel>
      <StackPanel Grid.Row="1">
      <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
            <Button Command="{Binding Start}" Height="45" Content="Start" FontSize="25" Margin="0,0,10,0"/>
            <Button Command="{Binding Stop}" Height="45" Content="Stop" FontSize="25" Margin="0,0,10,0"/>
      </StackPanel>
      <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
            <Slider Value="{Binding Slider}" Width="200" Height="20" Margin="0,10,0,0" />
            <Label Content="{Binding Path=SliderValue, UpdateSourceTrigger=PropertyChanged}" FontSize="18"/>
      </StackPanel>
</StackPanel>

Postępujemy analogicznie jak w przypadku pierwszego. Wypełniamy drugi następującym kodem:
<StackPanel Grid.Row="1">
       <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
               <Button Command="{Binding Start}" Height="45" Content="Start" FontSize="25" Margin="0,0,10,0"/>
               <Button Command="{Binding Stop}" Height="45" Content="Stop" FontSize="25" Margin="0,0,10,0"/>
        </StackPanel>
        <StackPanel  HorizontalAlignment="Center" Orientation="Horizontal">
                <Slider Value="{Binding Slider}" Width="200" Height="20" Margin="0,10,0,0" />
                <Label Content="{Binding Path=SliderValue, UpdateSourceTrigger=PropertyChanged}" FontSize="18"/>
         </StackPanel>
 </StackPanel> 
 Przy przyciskach pojawila się nowa właściwość – Command.
 Nasz widok powinien być już gotowy. Później będziemy musieli na chwile do niego wrócić alby połaczyć nasz ViewModel z przed chwila utworzonym widokiem. Budujemy nasz projekt (Ctrl+Shift+B)
 
3 Visual Studio
W tej części zajmiemy się logika naszej aplikacji. Otwieramy w Visual Studio projekt który wczesniej utworzyliśmy.  Na początku zajmiemy się utworzeniem naszego ViewModelu później przystąpimy do klas które będą mu potrzebne. Tworzymy nowa klasę :
 
Nazywamy ją MainViewModel i zatwierdzamy.  Nowo utworzona klasa musi implementować interfejs INotifyPropertyChanged. Do tego potrzebne będzie nam dodanie przestrzeni nazw (using System.ComponentModel) . Aby przyśpieszyć sobie prace Visual Studio zaimplementuje go za nas. Klikamy prawym przyciskiem myszki na przed chwila zadeklarowanej implementacji, wybieramy Implement Interface -> Implement Interface.
 
 Dodajemy metodę która będzie wywoływana przy każdej zmianie jakiejkolwiek właściwości. Będzie nam to potrzebne aby nasz widok mógł się zorientować, że zmiana miała miejsce i należy jego dane zaktualizować.
public void PropertyChangedInvoker(string name)
{
    if (PropertyChanged != null)
    {
         PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

Teraz kolej nadeszła na utworzenie właściwości w naszej klasie, dodajemy następujący kod:

//wartosc suwaka w postaci double
private double _slider;
public double Slider
{
     get { return _slider; }
     set
     {
          _slider = value;
          SliderValue = Convert.ToInt32(_slider);
     }
}

//pozycja suwaka w postaci int
private int _sliderValue;
public int SliderValue
{
     get { return _sliderValue; }
     set
     {
          _sliderValue = value;
          PropertyChangedInvoker("SliderValue");
     }
}

//liczby które są wyświetlane w widoku
private string _liczby;
public string Liczby
{
     get { return _liczby; }
     set
     {
          _liczby = value;
          PropertyChangedInvoker("Liczby");
     }
}

//odpowiada za trzymanie informacji czy przcisk Stop został nacisniety
private bool _stopButtonClicked;
public bool StopButtonClicked
{
     get { return _stopButtonClicked; }
     set
     {
          _stopButtonClicked = value;
          PropertyChangedInvoker("StopButtonClicked");
     }
}

//odowiada za trzymanie informacji czy przycisk Stop jest aktywny
private bool _canStopButtonExecute;
public bool CanStopButtonExecute
{
     get { return _canStopButtonExecute; }
     set
     {
          _canStopButtonExecute = value;
          PropertyChangedInvoker("CanStopButtonExecute");
     }
}

Naszemu ViewModelowi dalej czegoś brakuje. W Widoku utworzyliśmy 2 przyciski. Teraz pokaże co zrobić żeby je trochę ożywić. Pierwszym zadaniem będzie utworzenie i klas które będą implementowały interface ICommand. Klasy będą nazywać się odpowiednio StartCommand i StopCommand. Sama implementacja wygląda dokładnie tak samo jak wcześniej pokazałem. Wcześniej jednak do każdej z nich musimy dodać następującą przestrzeń nazw - using System.Windows.Input; Po tym nasze klasy, w tym wypadku StartCommnad, powinny wyglądać tak:

class
StartCommand : ICommand
{
    public bool CanExecute(object parameter)
    {
        throw new NotImplementedException();
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        throw new NotImplementedException();
    }
}

Klasa posiada dwie metody i jeden event. Pierwsza metoda(CanExecute) jest wywoływana gdy event (CanExecuteChanged) zostanie „wzniesiony”. Tak nasz widok sprawdza czy dana komenda może być wykonana.
Druga metoda (Execute) zawiera kod który będzie wywołany w przypadku gdy komenda ma zostać wykonana ( w naszym przypadku przez naciśniecie przycisku).
Musimy trochę rozbudować nasze klasy gdyż puste metody niewiele nam pomogą J. Pierwszym krokiem będzie utworzenie konstruktora i dodanie referencji do naszego ViewModelu dla naszych klas. Pozwoli nam to odwoływać się do jego właściwości.
private readonly MainViewModel _vm;
public StartCommand(MainViewModel viewModel)
{
    _vm = viewModel;
}
Dodamy teraz trochę logiki do naszych metod. Rozbudujemy tez nasz event.

public bool CanExecute(object parameter)
{
    return _vm.CanStopButtonExecute == false && _vm.SliderValue != 10 && _vm.SliderValue != 0;
}

public
event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

public
void Execute(object parameter)
{
    _vm.CanStopButtonExecute = true;
    System.Threading.ThreadPool.QueueUserWorkItem(_vm.Losowanie);
}
Wytłumaczenia wymagają dwie rzeczy. Pierwsza to co to jest losowanie. Losowanie to będzie metoda która będziemy jeszcze implementować w ViewModelu. Będzie ona odpowiedzialna za losowanie liczb. Drugą rzeczą dla niektórych może być tajemnicze wywołanie metody -System.Threading.ThreadPool.QueueUserWorkItem. Pozwala nam ono wykonać dana metodę w osobnym wątku które będzie „obliczać” swoje rzeczy niezależnie od głównego wątku naszej aplikacji. Więcej informacji znajdziecie we wcześniejszych wpisach z tamtego roku. Dlaczego tak ? Nie chcemy aby nad naszą aplikacja pojawiła się klepsydra :P Nie odpowiadałaby ona na nasze żądania.Teraz do rzeczy bo zostało nam trochę roboty jeszcze. Druga nasza klasa powinna wyglądać podobnie:

class
StopCommand : ICommand
{
    private readonly MainViewModel _vm;
    public StopCommand(MainViewModel viewModel)
    {
         _vm = viewModel;
    }

    public bool CanExecute(object parameter)
    {
         return _vm.CanStopButtonExecute;
    }

   
public event EventHandler CanExecuteChanged
    {
         add { CommandManager.RequerySuggested += value; }
         remove { CommandManager.RequerySuggested -= value; }
    }

   
public void Execute(object parameter)
    {
         _vm.StopButtonClicked = true;
    }
}

Musimy teraz „podpiąć” dopiero co stworzone klasy do ViewModelu. Przechodzimy do niego i  tworzymy kolejne dwie właściwości i w konstruktorze przypisujemy do nich nowe obiekty:

public
ICommand Start { get; set; }
public ICommand Stop { get; set; }
public MainViewModel()
{
     Start = new StartCommand(this);
     Stop = new StopCommand(this);
}

Dokładnie do tych dwóch właściwości bindowaliśmy nasze przyciski poprzez Command.
Kolejnym krokiem ( obiecuje ze już nie wiele tego zostało ) jest utworzenie klasy która będzie nam losować liczby o danej długości jak i metoda o której wcześniej wspomniałem. Zaczniemy od klasy.
Tworzymy i nazywamy ja Losomat. Oto logika która jest w niej zawarta:

public static class Losomat
{
    private const int max = 999999999;
    private const int min = 100000000;
    private static readonly Random Random = new Random();

   
public static string Losuj(int ilosc)
    {
       return
       Random.Next(Convert.ToInt32(min / Math.Pow(10, 9 - ilosc)), Convert.ToInt32(max / Math.Pow(10, 9 - ilosc))).ToString();
    }
}

Mam nadzieje że wszytko w niej jest jasne.
Weźmiemy się teraz za metodę. Przechodzimy do ViewModelu i dodajemy.

public
void Losowanie(object param)
{
    for (int i = 0; i < SliderValue; i++)
    {
        Liczby += "0";
    }
    for (int i = SliderValue; i > 0; i--)
    {
        while (!StopButtonClicked)
        {
            var temp = Losomat.Losuj(i);
            Liczby = Liczby.Substring(0, SliderValue - i) + temp;
            System.Threading.Thread.Sleep(10);
        }
        StopButtonClicked = false;
     }
     CanStopButtonExecute = false;
}

Rzeczą która może się przydać z tego kawałka kodu może być System.Threading.Thread.Sleep. Jest to metoda która usypia dany watek na podana ilość czasu. Czas podajemy w milisekundach.
Ostatnia rzeczą jaka musimy wykonać jest złączenie Widoku z ViewModelem. Przechodzimy do Widoku, przed Gridem dodajemy przestrzeń nazw i definiujemy zasób dla naszego okna.

xmlns:Haslo="clr-namespace:Haslo"
<Window.Resources>
     <Haslo:MainViewModel x:Key="ViewModel"/>
</Window.Resources>

 Łączymy nasz nowy zasób z Gridem poprzez DataContext:

<Grid x:Name="LayoutRoot" DataContext="{StaticResource ViewModel }" >

W ostateczność nasz początek kodu Widoku powinien wyglądać następująco :
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Haslo="clr-namespace:Haslo"
    x:Class="Haslo.MainWindow"
    x:Name="Window"
    Title="Haslo"
    Width="459" Height="204">    
    <Window.Resources>
           <Haslo:MainViewModel x:Key="ViewModel"/>
   </Window.Resources>
<Grid x:Name="LayoutRoot" DataContext="{StaticResource ViewModel }" >
Po tych wszystkich bojach możemy przystąpić do kompilacji naszego programu i cieszyć się jego możliwościami :D Kod z komentarzami będzie można pobrać z linku niżej.

Tagi: , ,

Ustalenia po spotkaniach organizacyjnych

Hej :)

W ubiegłym tygodniu odbyły się dwa, organizacyjne, spotkania grupy. Postanowiliśmy, że tak, jak w latach poprzednich, będą dwa typy spotkań: Warsztaty oraz Prezentacje. Na warsztatach, w tym semestrze będziemy uczyli się C# oraz pisali małe projekty. Spotkania zawsze będą odbywały się we wtorki (prezentacje) o godzinie 17.30 oraz w czwartki (warsztaty) o godzinie 19.

Zapraszam również na jutrzejsze spotkanie (wtorek 26.10), na którym Mateusz Jaskółowski opowie o tworzeniu aplikacji w WPF z użyciem wzorca MVVM. Opowie również o narzędziu do tworzenia wyglądu naszej aplikacji WPF. Zapraszamy!

Nie zapomnijcie zarejestrować się na: https://codeguru.pl/group-82/LectureDetails/1,4686.aspx

 

Tagi: , , ,

70-503: Synchronization

Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.

Ci z Was, którzy obsługiwali już wątki w .NET wiedzą, że nie jest to specjalnie skomplikowane. Najczęściej problemy występują przy obsłudze kontrolek Windows Forms, ponieważ ich właściwości mogą być zmieniane tylko w wątku, który je stworzył. Innym problemem jest wykorzystywanie lokalnej pamięci wątków do przechowywania informacji o kontekście, gdy proces nieoczekiwanie zmienia wątki, te dane mogą zniknąć. Z tej lekcji dowiemy się jak powyższe problemy są obsługiwane w WCFie.

Kontekst synchronizacji

W .NET 2.0 została wprowadzona rzadko używana funkcjonalność zwana kontekstem synchronizacji (klasa SynchronizationContext). Umożliwia ona sprawdzenie czy aktualnie wykonywany kod znajduje się w odpowiednim wątku. Aktualny kontekst możemy otrzymać odwołując się do statycznego pola SynchronizationContext.Current. Gdy jakaś metoda ma być wywołana w wątku, który nie jest bieżącym wątkiem, wywołujący wątek tworzy delegat typu SendOrPostCallback odwołujący się do żądanej metody a następnie jest przekazywany do metody Post (wywołanie asynchroniczne) lub Send (wywołanie synchroniczne) obiektu SynchronizationContext.

WCF i synchronizacja

A co ma kontekst synchronizacji do WCF’a? Nie wiem czy wiecie, że jeśli nie jest ustawione inaczej, każde wywołanie metod obiektu serwisu jest wykonywane przez wątki wejścia/wyjścia, z których żaden nie należy do naszej aplikacji. Gdybyśmy chcieli teraz zaktualizować coś w interfejsie użytkownika to napotkamy problem.

Z pomocą przychodzi nam właściwość UseSynchronizationContext klasy ServiceBehavior:

[ServiceBehavior(UseSynchronizationContext=true)]
public class UpdateService : IUpdateService

Ustawienie tego atrybutu na true spowoduje, że WCF będzie sprawdzał wątek uruchamiający hosta i jeśli wątek ten posiada kontekst synchronizacji i metoda serwisu jest wywołana z innego wątku, to będzie ona przekazywana do właściwego wątku i przez niego uruchomiona.

W przypadku hostowania serwisu w aplikacji Windows Forms/WPF, jeśli najpierw zostanie utworzony host przed oknem, nie zostanie utworzony żaden kontekst synchronizacji i każda aktualizacja kontrolek spowoduje błąd. Najpierw należy utworzyć okno a potem dopiero hosta serwisu.

Własny kontekst synchronizacji

WCF udostępnia tylko jedną klasę umożliwiającą obsługę kontekstu synchronizacji, dzięki której możemy np. aktualizować kontrolki w oknie aplikacji hostującej serwis. Możemy także utworzyć własne konteksty synchronizacji. Klasa kontekstu synchronizacjo odpowiada za wykonywanie metod serwisu przez konkretne wątki. Możemy wykorzystać ją np. do priorytetowania obsługi. Nasz kontekst synchronizacji może przekazywać wykonanie ważniejszych metod do wątków o wyższym priorytecie a pozostałych do innych wątków. Możemy właściwie zrobić o wiele więcej bazując na tym co udostępniają nam wątki.

Na początek potrzebna jest klasa bazująca na SynchronizationContext.

   1: public class ThreadPoolSynchronizer : SynchronizationContext, IDisposable
   2: {
   3:     Queue<WorkItem> workItemQueue;
   4:     WorkerThread[] workerThreads;
   5:     Semaphore itemAdded;
   6:  
   7:     public ThreadPoolSynchronizer(int poolSize)
   8:     {
   9:         if (poolSize <= 0)
  10:             throw new InvalidOperationException("Pool size cannot be zero");
  11:  
  12:         workItemQueue = new Queue<WorkItem>();
  13:  
  14:         workerThreads = new WorkerThread[poolSize];
  15:         for (int index = 0; index < poolSize; index++)
  16:             workerThreads[index] = new WorkerThread(index + 1, this);
  17:     }
  18:  
  19:     public void Close()
  20:     {
  21:         foreach (WorkerThread thread in workerThreads)
  22:             thread.Abort();
  23:     }
  24:  
  25:     public void Abort()
  26:     {
  27:         foreach (WorkerThread thread in workerThreads)
  28:             thread.Abort();
  29:     }
  30:  
  31:     public void Dispose()
  32:     {
  33:         this.Close();
  34:     }
  35:  
  36:     public override void Post(SendOrPostCallback method, Object state)
  37:     {
  38:         WorkItem workItem = new WorkItem(method, state);
  39:         QueueWorkItem(workItem);
  40:     }
  41:  
  42:     public override void Send(SendOrPostCallback method, Object state)
  43:     {
  44:         if (SynchronizationContext.Current == this)
  45:         {
  46:             method(state);
  47:             return;
  48:         }
  49:         WorkItem workItem = new WorkItem(method, state);
  50:         QueueWorkItem(workItem);
  51:         workItem.AsyncWaitHandle.WaitOne();
  52:     }
  53:  
  54:     protected Semaphore ItemAdded
  55:     {
  56:         get
  57:         {
  58:             if (itemAdded == null)
  59:                 itemAdded = new Semaphore(0, Int32.MaxValue);
  60:  
  61:             return itemAdded;
  62:         }
  63:         set
  64:         {
  65:             itemAdded = value;
  66:         }
  67:     }
  68:  
  69:     virtual internal void QueueWorkItem(WorkItem workItem)
  70:     {
  71:         lock (workItemQueue)
  72:         {
  73:             workItemQueue.Enqueue(workItem);
  74:             ItemAdded.Release();
  75:         }
  76:     }
  77:  
  78:     protected virtual bool QueueEmpty
  79:     {
  80:         get
  81:         {
  82:             lock (workItemQueue)
  83:             {
  84:                 if (workItemQueue.Count > 0)
  85:                 {
  86:                     return false;
  87:                 }
  88:                 return true;
  89:             }
  90:         }
  91:     }
  92:     internal virtual WorkItem GetNext()
  93:     {
  94:         ItemAdded.WaitOne(1000);
  95:         lock (workItemQueue)
  96:         {
  97:             if (workItemQueue.Count == 0)
  98:             {
  99:                 return null;
 100:             }
 101:             return workItemQueue.Dequeue();
 102:         }
 103:     }
 104:  
 105: }

Mamy tutaj prostą implementację własnej obsługi wątków. Dodatkowo utworzone są klasy WorkItem i WorkerThread (nazwy mówią same za siebie, poniżej pokażę ich kod).

Za funkcjonalność synchronizacji odpowiada pięć metod podzielonych na dwie grupy. Trzy metody operacyjne (Close, Abort, Dispose – linie 19-34) odpowiadają za zatrzymywanie wątku obsługującego żądanie. Dwie metody funkcyjne (Post i Send – linie 36-52) używane są przez WCF do uruchomienia metody. W metodzie Send (linie 44-48) sprawdzamy czy aktualny kontekst nie jest naszym kontekstem aby nie spowodować zakleszczenia.

Dla pełności przedstawiam jeszcze klasy WorkItem i WorkerThread:

   1:   [Serializable]
   2:   internal class WorkItem
   3:   {
   4:       object state;
   5:       SendOrPostCallback method;
   6:       ManualResetEvent asyncWaitHandle;
   7:  
   8:       public WaitHandle AsyncWaitHandle
   9:       {
  10:           get
  11:           {
  12:               return asyncWaitHandle;
  13:           }
  14:       }
  15:  
  16:       internal WorkItem(SendOrPostCallback method, object state)
  17:       {
  18:           this.method = method;
  19:           this.state = state;
  20:           asyncWaitHandle = new ManualResetEvent(false);
  21:       }
  22:  
  23:       internal void CallBack()
  24:       {
  25:           method(state);
  26:           asyncWaitHandle.Set();
  27:       }
  28:   }

 

 

 

   1: internal class WorkerThread
   2: {
   3:     ThreadPoolSynchronizer context;
   4:     public Thread threadObj;
   5:     bool endLoop;
   6:  
   7:     public int ManagedThreadId
   8:     {
   9:         get
  10:         {
  11:             return threadObj.ManagedThreadId;
  12:         }
  13:     }
  14:  
  15:     internal WorkerThread(int threadNumber, ThreadPoolSynchronizer context)
  16:     {
  17:         this.context = context;
  18:  
  19:         endLoop = false;
  20:         threadObj = null;
  21:  
  22:         threadObj = new Thread(Run);
  23:         threadObj.IsBackground = true;
  24:         threadObj.Name = "Tread-" + threadNumber.ToString();
  25:         threadObj.Start();
  26:     }
  27:  
  28:     bool EndLoop
  29:     {
  30:         set
  31:         {
  32:             lock (this)
  33:             {
  34:                 endLoop = value;
  35:             }
  36:         }
  37:         get
  38:         {
  39:             lock (this)
  40:             {
  41:                 return endLoop;
  42:             }
  43:         }
  44:     }
  45:  
  46:     void Start()
  47:     {
  48:         Debug.Assert(threadObj != null);
  49:         Debug.Assert(threadObj.IsAlive == false);
  50:         threadObj.Start();
  51:     }
  52:  
  53:     void Run()
  54:     {
  55:         Debug.Assert(SynchronizationContext.Current == null);
  56:         SynchronizationContext.SetSynchronizationContext(context);
  57:  
  58:         while (EndLoop == false)
  59:         {
  60:             WorkItem workItem = context.GetNext();
  61:             if (workItem != null)
  62:             {
  63:                 workItem.CallBack();
  64:             }
  65:         }
  66:     }
  67:  
  68:     public void Abort()
  69:     {
  70:         Debug.Assert(threadObj != null);
  71:         if (threadObj.IsAlive == false)
  72:         {
  73:             return;
  74:         }
  75:         EndLoop = true;
  76:  
  77:         threadObj.Join();
  78:     }
  79: }
  80:  

 

Teraz musimy przypisać nowy kontekst synchronizacji, może to wyglądać tak (przypisanie w linii 2):

   1: ThreadPoolSynchronizer syncContext = new ThreadPoolSynchronizer(3);
   2: SynchronizationContext.SetSynchronizationContext(syncContext);
   3: try
   4: {
   5:     ServiceHost host = new ServiceHost(typeof(UpdateService));
   6:     host.Open();
   7:     // Block until ready to quit
   8:     host.Close();
   9: }
  10: finally
  11: {
  12:     syncContext.Dispose();
  13: }

Innym lepszym sposobem jest udekorowanie klasy serwisu własnym atrybutem (tworzenie atrybutów wykracza poza ramy tego kursu). Może to wyglądać np. tak (atrybut z dwoma parametrami rozmiar puli oraz nazwa klasy serwisu):

[ThreadPoolSynchronization(3, typeof(UpdateService))]
[ServiceBehavior(typeof(IUpdateService))]
public class UpdateService : IUpdateService

Atrybut ten musi implementować interfejs IContractBehavior a w nim następujące metody:

  • AddBindingParameters – modyfikuje bindingi,
  • ApplyClientBehavior – modyfikuje lub rozszerza zachowanie serwisu dla wybranych lub wszystkich wiadomości,
  • ApplyDispatchBehavior - “wersja od strony serwisu” metody ApplyClientBehavior – rozszerza zachowanie dla przychodzących wiadomości,
  • Validate – potwierdza że kontrakt i punkt końcowy mogą obsłużyć zachowanie zaimplementowane w obiekcie.

Ponieważ chcemy zmienić  zachowanie po stronie serwisu, interesować nas będzie metoda AppliDispatchBehavior, która może być zaimplementowana np tak:

void ApplyDispatchBehavior(ContractDescription
description, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
{
    // ...
    if (dispatchRuntime.SynchronizationContext == null)
        dispatchRuntime.SynchronizationContext = new ThreadPoolSynchronizer(3);
    // ...
}

Synchronizacja i wywołania zwrotne

Obsługując wywołania zwrotne (ang. callback) także musimy brać pod uwagę problemy z współbieżnością. Podobnie jak w przypadku serwisu, u klienta możemy ustawić odpowiednie tryby synchronizacji, zarówno imperatywnie jak i deklaratywnie:

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Single)]
class CallbackClient : ICallback
{
    // Implementation code
}

Tak jak w ConcurrencyMode w klasie ServiceBehavior tak i tutaj mamy trzy możliwe wartości:

  • Single – tylko jedno wywołanie zwrotne jest możliwe w danym czasie, to gwarantuje nam że WCF nie wywoła metody więcej niż raz w tym samym czasie, nie gwarantuje natomiast że inne wątki klienta będą się odwoływać do zasobów używanych w tej metodzie, o to musimy sie już sami martwić;
  •  Multiple – dozwolone jest wielokrotne wywołanie metody, musimy sami postarać się o obsługę dostępu wielowątkowego;
  • Reentrant – metoda może być wywoływana ponownie przez serwis w tym samym wątku.

Wywołania zwrotne i konteksty synchronizacji

Możliwe jest także korzystanie z kontekstów synchronizacji w wywołaniach zwrotnych, wystarczy odpowiednio oznaczyć metodę:

[CallbackBehavior(UseSynchronizationContext=true)]
public class CallbackClient : ICallback
{
    // Implementation code
}

Na koniec jeszcze ważna informacja na temat wątków, wywołań zwrotnych i zakleszczeń. Załóżmy, że mamy przycisk uruchamiający metodę serwisu. Serwis dokonuje wywołania zwrotnego do aplikacji (dokładniej do wątku interfejsu użytkownika bo stamtąd pochodziło wywołanie serwisu). Wątek interfejsu użytkownika jest teraz zajęty bo czeka na odpowiedź od serwisu… No i mamy zakleszczenie. Jedynym rozwiązaniem tego problemu jest zrezygnowanie z kontekstu synchronizacji i ustawienie UseSynchronizationContext na false.

Na tym kończymy kurs.

Pozostańcie jeszcze z nami gdyż przygotowaliśmy małą niespodziankę.

Do zobaczenia na egzaminach :)

Tagi: , , , ,

70-503: Concurrency in WCF Applications

Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.

Współbieżność (ang. concurrency) w serwisie WCF występuje, kiedy jednocześnie więcej niż jedno wywołanie ma miejsce. Celem serwisu WCF jest przetwarzanie przychodzących żądań. Kiedy żądanie przychodzi do serwisu, serwis rozdziela (ang. dispatch) komunikaty na własne wątki, które brane są z puli wątków. Z każdym żądaniem powiązany jest obiekt serwisu – instancja klasy, która implementuje interfejs serwisu. W WCF kwestia współbieżności zależy od tego, jak te obiekty są tworzone i dzielone pomiędzy pojedyncze żądania.

WCF przewiduje trzy możliwe tryby dzielenia obiektu serwisu:

  • Single – każdy wątek, który obsługuje żądanie może mieć dostęp do obiektu serwisu, ale tylko jeden w danym czasie, używanie trybu Single zmniejsza ilość problemów związanych z współbieżnością,
  • Reentrant – tylko jeden wątek może mieć dostęp do obiektu serwisu w danym czasie, jednak ma on możliwość opuszczenia obiektu i powrotu do niego w późniejszym czasie,
  • Multiple – obiekt serwisu obsługuje jednocześnie wiele żądań; jest to najtrudniejszy tryb do implementacji, ponieważ wymaga wielkiej staranności przy korzystaniu z zasobów dzielonych (ang. shared resources).

Tryb współbieżności jest ustawiany korzystając z atrybutu ServiceBehavior - ConcurrenczMode, na klasie, która implementuje serwis.

Tryb współbieżności Single

Ustawienie ConcurrencyMode na wartość ConcurrencyMode.Single gwarantuje najbardziej bezpieczne środowisko dla współbieżności.

   1: [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Single)]
   2: public class ServiceImplementation : IServiceInterface
   3: {
   4:   // TODO: Implementacja...
   5: }

Przed rozpoczęciem przetwarzania żądania, na obiekt serwisu zakładana jest blokada, która zdejmowana jest dopiero po zakończeniu operacji. Jeżeli w tym czasie przyjdą kolejne żądania, zostaną one odłożone na kolejkę (FIFO – first-in, first-out) i czekają, kiedy obiekt serwisu będzie dostępny. Przetwarzanie pojedynczego żądania w danym czasie eliminuje problemy zarządzanie współbieżnością. Jedyną sytuacją, kiedy może pojawić się problem jest sytuacja, gdy obiekt serwisu wykonuje operacje wielowątkowe. Istnieje tu jednak pewien kompromis, którym zależy od trybu wystąpienia. Innymi słowy należy uwzględnić związek pomiędzy ConcurrencyMode a InstanceContextMode.

Przykład: Zakładamy, że mamy podaną gotową implementację serwisu. Jak możemy udekorować klasę, aby wyeliminować współbieżność, nie modyfikując niczego wewnątrz klasy?

   1: [ServiceBehavior()]
   2: public class ServiceImplementation : IServiceInterface
   3: {
   4:   private static int hitCounter;
   5:   public void Increment()
   6:   {
   7:     hitCounter++;
   8:   }
   9: }

Możemy ustawić ConcurrencyMode na Single a InstanceContextMode na PerSession lub Single. Wykorzystanie trybu współbieżności Single zapewnia, że tylko jedno żądanie może być przetwarzane w danej chwili. Połączenie ConcurrencyMode w trybie Single i InstanceContextMode w trybie Single daje gwarancję, że jest tworzona tylko jedna instancja serwisu. To połączenie jest wymagane w przypadku operacji ze statycznymi lub dzielonymi zmiennymi, aby były zabezpieczone na naruszenia współbieżności.

Tryb Single cechuje potencjalnie niska przepustowość spowodowana przesyłaniem przychodzących żądań przez jeden obiekt.

Tryb współbieżności Multiple

W przypadku serwisów, które wymagają większej przepustowości dostępny jest model wielowątkowy. Kiedy ConcurrencyMode jest ustawiony na Multiple, nie występuje już zakładanie blokady na obiekt serwisu przed obsłużeniem żądania, a obiekt serwisu może (w zależności od trybu wystąpienia) obsłużyć wiele żądań jednocześnie. Informacja o stanie serwisu i dzielonych zasobach musi być chroniona poprzez wykorzystanie standardowych technik synchronizacji, które oferuje .NET framework.

Tryb współbieżności Reentrant

Serwis po ustawienie trybu na Reentrant zachowuje się tak samo jak w przypadku trybu Single. Przed przetworzeniem żądania zakładana jest blokada na serwis i trzymana jest tak długo, jak długo trwają operacje. Różnica leży w tym, co może się wydarzyć podczas samego przetwarzania.

imagePodczas przetwarzania żądania przez serwis może wystąpić sytuacja, że serwis musi wykonać operację na innym serwisie. Inne żądania do serwisu muszą czekać na zakończenie aktualnie przetwarzanego przez serwis żądania, gdzie serwis czeka na zakończenie wywołania, które sam rozpoczął. Problem ten ilustruje zamieszczony rysunek.

Co się teraz stanie, jeżeli zewnętrzny serwis wywoła żądanie na naszym serwisie WCF? Żądanie zostanie dodane do kolejki, obiekt serwisu nie zostanie odblokowany, wystąpi zakleszczenie! (WCF jest w stanie poradzić sobie z tym przez unieważnienie żądanie po pewnym czasie – timeout, lub rzucenie wyjątku InvalidOperationException). Tryb Reentrant rozwiązuje ten problem.

Różnica między trybem Single a Reentrant jest taka, że w tym drugim trybie, kiedy obiekt serwisu wykonuje własne żądanie zdejmowana jest blokada. Pozwala to na obsłużenie innych żądań. Kiedy odpowiedz na żądanie serwisu nadejdzie, zostanie dodana do kolejki razem z innymi żądaniami. Kiedy zacznie się jego przetwarzanie, założy blokadę na obiekt serwisu i dokończy swoje zadanie.

Chociaż konfiguracja serwisu do działania w trybie reentrant jest prosta, programista musi musi liczyć się z dużą odpowiedzialnością wiążącą się z tym rozwiązaniem. Zakleszczenia są problemem, jednak nie jedynym. Programista musi zapewnić, że kiedy serwis wykonuje żądanie, stan serwisu musi pozostać spójny – serwis nie powinien oddziaływać ze swoimi własnymi polami (publicznymi, czy prywatnymi, należącymi do instancji, czy statycznymi) w taki sposób, aby jakikolwiek obiekt pozostał w nieakceptowanym stanie. Przykładowo, jeżeli budujemy strukturę drzewiastą, nie możemy dopuścić do tego, aby korzeń drzewa nie był zdefiniowany.

W następnej, ostatniej już lekcji, zapoznamy się z synchronizacją.

Tagi: , , , , ,

70-562: Using Caching to Improve Performance

Artykuł pochodzi w serii przygotowań do egzaminu 70-562 ASP.NET.

Często buforujemy dane na stronie bądź cała stronę co pozwala na szybszy dostęp do informacji niż z pliku czy bazy danych. Poprawia to oczywiście wydajność i skalowalność jeśli chodzi o liczbę użytkowników obsługiwanych na WWW. Nasz wspaniałomyślny ASP.NET bez większego nakładu pracy (czyt. pisania kodu) pozwala obsłużyć pamięć podręczną.  Wyróżniamy dwa rodzaje takiej pamięci:

Application caching- kolekcja ta może przechowywać dowolny obiekt, automatycznie zarządza pamięcią, limitami czasu przechowywania obiektu oraz innymi zależnościami.

Page output caching- pozwala przechowywać stronę, jej część lub wersje w pamięci co pozwala skrócić czas dostępu do niej.

Application Caching

Jest to proces przechowywania danych w którym mamy dostępny obiekt Cache który jest właściwością obiektu Page. Stanowi on zbiór klasy typu System.Web.Caching.Cache. Obiekt ten wykorzystuje całą pamięć podręczną co oznacza, że jest on jeden na cała aplikacje a nie na stronę. Poniższy rysunek pokazuje nam ten obiekt:

1

Praca z obiektem Cache jest bardzo podobna do pracy z sesją. Można przypisać element bezpośrednio nadając mu klucz i wartość. Przy pobieraniu wartość z pamięci podręcznej pamiętajmy aby sprawdzić czy nie jest ona null (mógł np. upłynąć czas przechowywania wartości). Poniższy przykład demonstruje jak pobrać obiekty string z pamięci:

   1: //C#
   2: Cache["Greeting"] = "Hello, world!";
   3:     if (Cache["Greeting"] != null)
   4:         value = (string)Cache["Greeting"];
   5:     else
   6:         value = "Hello, world!";

Oczywiście to jest najprostszy sposób i wydaje się mało rzeczywisty ale równie dobrze możemy przechowywać tam pliki, wyniki zapytań czy własne obiekty. Musimy tylko pamiętać aby zrzutować pobieraną wartość na odpowiedni typ.  W TK są opisane właściwości którymi możemy rozszerzyć o pewne właściwości nasze przechowywane dane i przykłady. Ja chciałbym przytoczyć jeden w którym jest ustawiany czas “trzymania” wartości ;)

   1: //C#
   2: Cache.Insert("FileCache", "CacheContents", null, DateTime.Now.AddMinutes(10),
   3: Cache.NoSlidingExpiration);

Page Output Caching

Często jest tak, że przeglądarka pobiera stronę, zapisuje ją na dysku i przy żądaniu sprawdza czy jest nowa wersja i jeżeli nie to ją wczytuje. Taki rozwiązanie m.in powoduje mniejsze obciążenie serwera. Aby zwiększyć wydajność i zmniejszyć czas renderowania, ASP.NET obsługuje tytułowy page output caching. Powoduje to, że np. serwer może trzymać w swojej pamięci zażądaną stronę i nawet kiedy inny użytkownik będzie chciał ją wczytać otrzyma ją w bardzo szybkim tempie. Jest to przydatne kiedy dana strona jest “ciężka”. Nie ma również problemu ze stornami tworzonymi dynamicznie wg. indywidualnych potrzeb użytkownika.

Możemy każdej ze stron ustawić buforowanie. Robimy to dodając do dyrektywy @ OutputCach. W TK są właściwości jakie możemy ustawić czyli np. czas, lokalizacje przechowywania oraz których nie możemy używać do kontrolek. Poniższy przykład pokazuje, jak ustawić “cachowanie” strony przez 15 minut, niezależnie od parametry przekazane do strony:

   1: <%@ OutputCache Duration="15" VaryByParam="location;count" %>

Nic nie stoi na przeszkodzie aby ustawić buforowanie dla całej aplikacji. Robimy sobie profile które potem możemy użyć na poszczególnych stronach. Oczywiście musimy umieścić odpowiednią sekcje w naszym web.config :

   1: <caching>
   2:     <outputCacheSettings>
   3:         <outputCacheProfiles>
   4:             <add name="OneMinuteProfile" enabled="true" duration="60"/>
   5:         </outputCacheProfiles>
   6:     </outputCacheSettings>
   7: </caching>

I aby nasza strona stosowała się do tego musimy przypisać jej ten profil:

   1: <%@ OutputCache CacheProfile="OneMinuteProfile" VaryByParam="none" %>

Na tym zakończę ten wpis. Wybaczcie, że jest on taki ogólnikowy ale jestem po nocnej podróży z Imagine Cup ;) W TK możecie doczytać sobie różne właściwości i np. jak sprawdzić czy skorzystać z zapisanej lokalnie strony czy nie ;) Jest to również ostatni wpis z serii przygotowań do egzaminu 70-562 ASP.NET. Wszystkim wiernym czytelnikom dziękuje w imieniu chłopaków jak i swoim :) Pamiętajcie, że artykuły nakreślają tematy zawarte w TK i mają tylko pomóc w zrozumieniu pewnych zagadnień a nie są kompendium wiedzy dotyczącej samego egzaminu i jego zawartości. Trzymajcie się, miłego weekendu! :D

Tagi: , , , ,

70-503: Programming Transactions

Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.

No to wiemy już jak włączyć transakcje i co trzeba zrobić, zarówno po stronie serwisu jak i po stronie klienta, aby informacje o transakcji były przekazywane w obie strony. Dzisiaj dowiemy się więcej o obsłudze transakcji od strony kodu.

Transakcje otoczenia

W .NET Framework 2.0 w przestrzeni nazw System.Transaction zostały wprowadzone tzw. transakcje otoczenia (ang. Ambient Transactions). Polega to na tym, że transakcja istnieje w aktualnym wątku lub w kontekście obiektu i wszystkie działania w ramach tego wątku lub kontekstu wchodzą w skład tej transakcji (o ile taka transakcja została w ogóle rozpoczęta).

Aby sprawdzić czy mamy dostępną transakcję otoczenia sprawdzamy statyczne pole Current klasy Transaction:

Transaction ambientTransaction = Transaction.Current;

Jeśli nie ma transakcji, Transaction.Current jest równe null.

Jeśli nasz kod zostanie wywołany poprzez zdalnego klienta (przez binding w którym jest włączone przekazywanie transakcji) to ta lokalna transakcja stanie się transakcją rozproszoną (ang. distributed transaction).

Klasa Transaction (i obiekt Transaction.Current) udostępnia właściwość TransactionInformation, z której możemy dowiedzieć się jaka jest “lokalność” transakcji. Właściwość TransactionInformation zawiera dwa pola: LocalIdentifier – string zawierający unikalny identyfikator aktualnej transakcji, oraz DistributedIdentifier – string zawierający globalny unikalny identyfikator transakcji rozproszonej (GUID). Jeśli transakcja nie została podniesiona do rangi rozproszonej transakcji, wartość DistributedIdentifier będzie równa Guid.Empty.

Klasa TransactionScope

Jak sama nazwa wskazuje, klasa TransactionScope definiuje przestrzeń transakcji:

using (TransactionScope ts = new TransactionScope())
{
    //
    // tutaj aktualizujemy dane w systemie, itp.
    //
 
    ts.Complete();
}

W zależności od tego czy nasz kod jest wykonywany w ramach transakcji otoczenia (ang. ambient transaction) to, albo jest tworzona nowa transakcja LTM (jeśli nie ma transakcji otoczenia), albo to co robimy w ramach tej przestrzeni transakcji jest podłączane pod aktywną transakcję otoczenia.

Istotny jest sposób w jaki zakańczana jest transakcja. Transakcja istnieje dopóki istnieje obiekt klasy TransactionScope (dlatego jest ważne jest użycie using, lub jawne zwolnienie obiektu). Jeśli nie zwolnimy sami obiektu transakcji, może on nie być w ogóle zwolniony (czyli będzie aktywny przez całe działanie serwisu – o tym decyduje odśmiecacz (ang. garbage collector)), a nasza transakcja zakończy się niepowodzeniem przez przekroczenie czasu (ang. timeout).

Jeśli przed zwolnieniem obiektu klasy TransactionScope nie zostanie wywołana metoda Complete() całość transakcji zostanie wycofana (ang. rollback). Jeśli metoda Complete() zostanie wywołana, przy zwalnianiu obiektu cała transakcja zostanie zatwierdzona (ang. commit).

Głosowanie w transakcjach

Tak jak zostało powiedziane w poprzedniej lekcji, transakcje LTP charakteryzują się dwufazowym procesem zatwierdzania transakcji. Wywołanie metody Complete() obiektu TransactionScope nie gwarantuje zatwierdzenia transakcji, informuje ono tylko menadżera transakcji o tym, że u nas jest wszystko w porządku.

W przypadku gdy inny uczestnik transakcji zgłosi niepowodzenie, przy wywołaniu u nas metody Complete() dostaniemy wyjątek TransactionAbortedException. Tak więc obsługę transakcji musimy uzupełnić o obsługę tego wyjątku:

try
{
    using(TransactionScope ts = new TransactionScope( ))
    {
        /* Perform updates here */
        ts.Complete( );
    }
}
catch(TransactionAbortedException e)
{
    /* Rollback updates, if necessary */
}

Zagnieżdżanie transakcji

Domyślnie utworzenie nowej transakcji spowoduje podpięcie się pod aktualną transakcję, ewentualnie jeśli takiej nie ma to  utworzenie nowej. Jeden z konstruktorów klasy TransactionScope przyjmuje parametr typu TransactionScopeOption, który pozwala nam decydować czy chcemy utworzyć transakcję zagnieżdżoną czy dołączyć się do bieżącej transakcji, Wartości TransactionScopeOption:

  • Required – użyta jest transakcja otoczenia, lub stworzona nowa transakcja jeśli transakcja otoczenia nie istnieje, to jest wartość domyślna (tworząc obiekt klasy TransactionScope chcemy fragment kodu objąć transakcją, bez względu na to czy transakcja już istnieje czy nie),
  • RequiresNew – zawsze jest tworzona nowa transakcja i jest ona jednocześnie transakcją główną (ang. root transaction) dla wszystkich następnych,
  • Suppress – nawet jeśli istnieje transakcja otoczenia, żadne zmiany w tym fragmencie kodu nie zostaną uwzględnione w transakcji, ta opcja służy do wykonania kodu poza transakcją, wszelkie próby kontaktu z kodem działającym w transakcjach (np. wywołanie metody serwisu, która działa w transakcji spowoduje błąd)

Przykład zagnieżdżonych transakcji:

using(TransactionScope ts1 = new TransactionScope())
{
    using(TransactionScope ts2 = new TransactionScope())
    {
        ts2.Complete();
    }
    ts1.Complete();
}

Aby obie transakcje były zatwierdzone, metoda Complete() musi być wykonana dwukrotnie (raz dla każdego obiektu). Gdyby transakcja ts1 nie została zatwierdzona, żadne zmiany (nawet te zatwierdzone przez ts2) nie zostaną wprowadzone:

using(TransactionScope ts1 = new TransactionScope())
{
    using(TransactionScope ts2 = new TransactionScope())
    {
        ts2.Complete();
    }
}

Kolejnym zagadnieniem jest izolacja transakcji. Niektóre konstruktory klasy TransactionScope przyjmują jako parametr strukturę TransactionOptions, w której jedną z właściwości jest IsolationLevel:

  • Serializable – najwyższy poziom, inni użytkownicy nie widzą danych modyfikowanych w ramach transakcji, inne procesy nie mogę zmienić lub dodać danych będących w konflikcie z tą transakcją,
  • RepeatableRead – inne procesy mogą oglądać dane modyfikowane przez tą transakcję, ale te dane nie mogą być modyfikowane, ale mogą być dodane nowe dane które będą widoczne w transakcji,
  • ReadCommitted – inne procesy nie mogą oglądać niezatwierdzonych (ang. uncommited) danych,
  • ReadUncommitted – inne procesy mogą odczytywać i modyfikować niezatwierdzone dane,
  • Snapshot -  dane mogą być odczytywane, przed zatwierdzeniem transakcji sprawdzane jest czy w trakcje transakcji nie zostały zmienione dane (np. przez procesy zewnętrzne) poprzednio wczytane i modyfikowane w ramach transakcji
  • Chaos – zmiany z bardziej izolowanych transakcji nie mogą być zastępowane,
  • Unspecified – używany jest inny poziom izolacji, którego nie można określić, ustawienie takiej wartości spowoduje wyrzucenie wyjątku

Zagnieżdżone transakcje muszą używać tego samego poziomu izolacji jeśli chcą dołączyć do transakcji otoczenia (ang. ambient transaction), w przeciwnym przypadku wystąpi wyjątek ArgumentException.

Limity czasu transakcji

Możemy ustawić maksymalny czas trwania transakcji. Poniżej konstruktor z ustawionym czasem:

TransactionScope ts = new TransactionScope(TransactionScopeOption.Required,
    new TimeSpan(0, 10, 0));

Możemy też ustawić nieskończony czas trwania (co oczywiście jest dość ryzykowne):

TransactionScope ts = new TransactionScope(TransactionScopeOption.Required,
    TimeSpan.Zero);

Popatrzmy jeszcze na przykład zagnieżdżonych transakcji:

   1: using(TransactionScope ts1 = new
   2:     TransactionScope(TransactionScopeOption.Required,
   3:         new TimeSpan(0, 2, 0) ))
   4: {
   5:     // A transaction with a timespan of 2 minutes is created
   6:     using(TransactionScope ts2 = new
   7:         TransactionScope(TransactionScopeOption.Required,
   8:             new TimeSpan(0, 1, 0) ))
   9:     {
  10:         // A transaction with a timespan of 1 minute is created
  11:         Thread.Sleep(90000);
  12:     }
  13: }

Tutaj transakcja ts1 ma timeout 2 minuty (linia 3), a transakcja ts2: 1 minuta (linia 8). W transakcji ts2 występuje operacja (linia 11) trwająca dłużej niż 2 minuty i to transakcja ts2 spowoduje wycofanie całego bloku (włącznie z ts1).

Na koniec jeszcze informacja jak ustawić domyślny timeout:

<system.transactions>
    <defaultSettings timeout="00:00:10" />
</system.transactions>

W ostatnich lekcjach tego kursu powiemy odrobinę o współbieżności.

Tagi: , , , ,

Eastgroup.pl na facebooku