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: , , , ,

70-562:Deploying Web applications

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

W dzisiejszej lekcji powiemy sobie o wdrażaniu aplikacji internetowych w ASP.NET.

Tworzenie projektu Web Setup

Web setup jest narzędziem, które wspomaga wdrażanie naszej aplikacji internetowej. Projekt Web Setup jest bardzo podobny do standardowego projektu Setup, który mamy w aplikacjach Windows Forms, lecz dostarcza specjalnych właściwości wymaganych przez aplikację Web. Aby stworzyć nowy projekt typu Web Setup musimy podczas tworzenia projektu wybrać zakładkę Other Project Types –> Setup  and Deployment –> Web Setup Project.
image
Po stworzeniu nowego projektu, Visual dołączy projekt do solution oraz wyświetli edytor “File System”. Następnie musimy dodać do nowo utworzonego projektu nowy element. Ustawiamy się na projekcie i wybieramy Add-> Project Output. W oknie tworzenia Project Output wybieramy Content i configuration ustawiamy na Active. W tym momencie zapewniliśmy sobie dostęp do naszego projektu, który chcemy wdrażać.

Tworzenie warunków uruchomienia

Dzięki warunkom uruchomienia możemy sobie sprawdzać np. czy na serwerze znajdują się Service Packi albo inne komponenty, które są wymagane do wdrożenia naszej aplikacji. Do tworzenia i zarządzania warunkami uruchomienia używa się edytora Launch Condition. Aby z niego skorzystać klikamy prawym przyciskiem na projekcie, następnie wybieramy View-> Launch Condition. Teraz aby dodać nowy warunek klikamy prawym przyciskiem na folderze Launch Condition i wybieramy Add Launch Condition:
image

 

Istnieją dwie główne gałęzie w edytorze: Search Target Machine oraz Launch Condition:
Search Target Machine – pozwala na zdefiniowanie kryteriów wyszukiwania przed instalacją. Domyślnie zawiera wyszukiwanie dla IIS ale możemy dodać do niego pliki rejestru, Windows Installer Search Condition itp.
Launch Condition – pozwala na tworzenie nowych warunków, które muszą być spełnione przed instalacją. Mogą być one oparte o warunki wyszukiwania lub inne kryteria (np. wersja systemu operacyjnego).

Po stworzeniu nowego warunku, możemy we właściwości Condition ustawić nasz warunek. Np. sprawdzanie wersji IIS może odbyć się w taki sposób:

   1: IISVERSION = "#6" 

Sprawdzamy tu, czy wersja IIS jest równa 6. Oczywiście możemy używać operatorów > < >= <=. Aby sprawdzić czy dyskiem domowym jest C:

   1: %HOMEDRIVE = "C:"

Dodawanie własnej strony setup’u

Możemy dodać własne strony do naszego instalatora. Można w nich pobierać jakieś informacje a następnie przekazywać je jako parametry do własnych akcji. Działania jakie możemy wykonywać podczas instalacji:
Wyświetlanie licencji – projekt Web Setup dostarcza szablon do potwierdzania licencji
Modyfikowanie ustawień w Web.Config – zmienianie ustawień Web.Config na podstawie danych przekazanych przez użytkownika
Wykonywanie niestandardowej konfiguracji – możemy za pomocą własnej konfiguracji zapytać użytkownika o jakieś informację i przechowywać je np. w rejestrze.
Aktywacja bądź rejestracja aplikacji – możemy wymagać od użytkownika podanie klucza bądź rejestracji

Aby dodać własną stronę, wchodzimy w User Interface(prawym przyskiem myszy na projekcie –> View –> User Interface). Edytor ten wyświetla różne etapy konfiguracji itp:
image

Teraz wystarczy PPM na etapie, w którym chcemy dodać stronę i wybieramy Add Dialog. Widzimy opcję, które możemy dodać do projektu. Proponuję pobawić się nimi i zobaczyć jak działają bądź doczytać o nich na msdnie.
image

Wdrażanie aplikacji

Po skonfigurowaniu naszego projektu i zbudowaniu jesteśmy gotowi wdrożyć go na serwerze :) Aby to zrobić, należy użyć plików wygenerowanych podczas buildu:
Setup.exe – plik, który instaluje pliki i wprowadza ustawienia, które dodaliśmy w naszym projekcie. Podczas instalacji zostaniemy zapytani o wszystkie dodatkowe ustawienia.
<NazwaProjektu>.msi – plik instalatora Windows zawierający wszystkie pliki dodane do projektu. Uruchomienie tego pliku jest równoważne uruchomieniu Setup.exe. Ten typ pliku jest mniejszy i bardziej uniwersalny.
Bardziej polecany jest plik msi, chociaż ma jedną wadę: serwer, na którym chcemy instalować aplikację musi mieć Windows Installer.

To wszystko z mojej strony :) Dziękuję za wytrwanie w tej serii wpisów :)

 

Tagi: , , , , ,

70-503: Transaction Basics

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

Podstawową funkcją transakcji jest zagwarantowanie zasad ACID:

  • atomowości (ang. atomicity),
  • spójności (ang. consistency),
  • izolacji (ang. isolation),
  • trwałości (ang. durability).

Kiedy operacje związane z bazą odbywają się na wielu maszynach i wielu zbiorach danych, nie jest to takie proste. WCF wspomaga programistę w tym zadaniu.

W celu spełnienia zasad ACID najczęstszym podejściem jest wykorzystanie dwuetapowego zgłoszenia (ang. two-phase commit):

  1. Etap przygotowania (ang. prepare phase) – koordynator transakcji zarządza tym etapem, wysyła żądanie przygotowania do wszystkich zarządców transakcji (ang. transaction manager), na maszynach biorących udział w procesie. Zarządcy odsyłają informację o tym, czy operacje zakończyły się sukcesem, czy porażką. Kiedy wszyscy odpowiedzą etap przygotowania uznajemy za zakończony.
  2. Etap zgłoszenia (ang. commit phase) – zależny od wyników etapu poprzedniego; jeżeli tamten zakończy się sukcesem – wysyłane zostaje żądanie Commit, w przeciwnym wypadku, jeżeli chociaż jedna maszyna zwróci komunikat błędu – koordynator transakcji wysyła żądanie Abort, aby poinformować zarządców o potrzebie cofnięcia zmian.

W zależności od sytuacji wykorzystany może zostać jeden z trzech zarządców transakcji:

  • The Lightweight Transaction Manager (LTM) – wprowadzony w .NET 2.0 przez przestrzeń nazw System.Transaction (do projektu trzeba dodać assembly); poniższy przykład aktualizuje bazę danych w ramach lekkiej transakcji. W celu zgłoszenia transakcji wywołana jest metoda Complete:
  •    1: using (TransactionScope ts = new TransactionScope())
       2: {
       3:   using (SqlConnection cn1 = new SqlConnection(connectionString))
       4:   {
       5:     insertRecord(cn1, "User1");
       6:     using(SqlConnection cn2 = new SqlConnection(connectionString))
       7:     {
       8:       insertRecord(cn2, "User2");
       9:     }
      10:   }
      11:   ts.Complete();
      12: }
      13: private void insertRecord(SqlConnection cn, string userName)
      14: {
      15:   SqlCommand cmd = new SqlCommand(String.Format("Insert INTO [Users]" +" VALUES('{0}')", userName), cn);
      16:   cn.Open();
      17:   cmd.ExecuteNonQuery();
      18: }
  • OLE Transactions (OleTx),
  • WS-Atomic Transactions (WS-AT).

Transakcji możemy pozwolić na działanie poza granicami serwisu, lub nie. Decyzję o tym podejmują klient i serwis, jednak jeżeli serwis wymaga transakcji, blokowanie po stronie klienta spowoduje błąd aplikacji. Ustawione może to zostać w bindingu za pomocą atrybutu TransactionFlow, zarówno imperatywnie jak i deklaratywnie:

   1: // C#
   2: WSHttpBinding binding = new WSHttpBinding();
   3: binding.TransactionFlow = true;
   4:  
   5: <!--XML-->
   6: <bindings>
   7: <wsHttpBinding>
   8: <binding name="Transactional" transactionFlow="true" />
   9: </wsHttpBinding>
  10: </bindings>

 

 

Dodatkowo wymagane jest, aby operacje serwisu były oznaczone atrybutem TransactionFlow, który wskazuje na to, że mogą one brać udział w transakcji:

   1: [ServiceContract]
   2: public interface IDemoContract
   3: {
   4:   [OperationContract]
   5:   [TransactionFlow(TransactionFlowOption.Allowed)]
   6:   void TransactedMethod(...);
   7: }

Po stronie klienta w ten sam sposób zostanie udekorowana klasa proxy:

   1: public class DemoService : IDemoContract
   2: {
   3:   [TransactionFlow(TransactionFlowOption.Allowed)]
   4:   public void TransactedMethod(...)
   5:   {...}
   6: }

W powyższym przykładzie TransactionFlow ustawiony jest na wartość TransactionFlowOption.Allowed, która pozwala transakcji klienta przechodzić do serwisu. Domyślna opcja TransactionFlowOption.NotAllowed sprawia, że żadna transakcja nie jest przesyłana.

Uwaga: Transakcje nie działają w przypadku metod typu one-way – bez komunikatu zwrotnego nie ma możliwości stworzenia rozproszonej transakcji.

Tagi: , , , ,

70-562: Building Mobile Application

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

W dzisiejszej lekcji będzie na temat budowania, uruchamiania oraz testowania mobilnych wersji aplikacji webowych stworzonych w technologii ASP.NET.

Tworzenie aplikacji mobilnych mocno nie różni się od tworzenia zwykłych aplikacji webowych. Trzeba tylko pamiętać, że w większości urządzenia mobilne mają większe ograniczenia w stosunku do normalnych komputerów. Dlatego wersje mobilne aplikacji powinny być jak najmniej skomplikowane oraz zawierać jak najmniej elementów, aby urządzenie poradziło sobie z jej wyświetleniem.

Dodanie mobilnej strony do aplikacji

Aplikacja ASP.NET może zawierać jednocześnie strony w wersji normalnej oraz wersje mobilne. Nie ma oddzielnego projektu w Visual Studio dla mobilnych aplikacji ASP.NET. Nie ma również szablonu dla strony mobilnej, który można by wykorzystać dodając taką stronę do projektu. W celu dodania mobilnej strony, programista musi dodać normalną stronę, a następnie zmienić trzy elementy na niej:

  • w kodzie behind danej strony zmieniamy klasę, z której dziedziczy klasa strony z System.Web.UI.Page na System.Web.UI.MobileControls.MobilePage
  • w kodzie aspx po dyrektywie page dodajemy dyrektywę, która załaduje przestrzeń nazw System.Web.UI.MobileControls, z której będą pochodzi kontrolki na stronie
       1: <%@ Register TagPrefix="mobile" Namespace="System.Web.UI.MobileControls" %>
  • zamieniamy wygenerowany przez Visual Studio formularz, który korzysta z wcześniej dodanej przestrzeni nazw:
   1: <mobile:Form id="form1" runat="server">
   2: </mobile:Form>

Mobilne kontrolki

Przestrzeń nazw System.Web.UI.MobileControls udostępnia programiście szereg kontrolek, które może wykorzystać tworząc mobilne wersje aplikacji ASP.NET, które są przystosowane do pracy z urządzeniami mobilnymi. Tymi kontrolkami są między innymi (większość kontrolek jest bardzo podobna do normalnych kontrolek ASP.NET):

  • Label – za jej pomocą programista może z poziomu kodu behind wyświetlić tekst na stronie. Gdy ma zostać wyświetlona większa ilość tekstu, lepiej skorzystać z kontrolki TextView
  • TextBox – służy do pobierania danych tekstowych od użytkownika
  • TextView – służy do wyświetlenia większej ilości tekstu. Umożliwia użycia prostego formatowania HTML (np. a (link), b (pogrubienie), br (znak nowej lini), i (kursywa), p (akapit))
  • Command – przycisk, który może kliknąć użytkownik. Wywoływane jest zdarzenie OnClick, gdy użytkownik kliknie w przycisk
  • Image – kontrolka służy do wyświetlenie obrazka, w przypadku, gdy format obrazka nie jest obsługiwany, wyświetlany jest tekst z właściwości AlternateText
  • List – kontrolka służy do wyświetlenie elementów w formie listy
  • SelectionList – jest podobna do kontrolki List, dodatkowo umożliwia wielokrotny elementów z listy
  • ObjectList – jest to odpowiednik GridView z normalnego ASP.NET, czyli umożliwia wyświetlenie danych w postaci tabelki
  • Calendar – kontrolka kalendarza umożliwiająca wybranie daty przez użytkownika. Kontrolka może wyglądać różnie w zależności od urządzenia (patrz opis niżej)
  • Kontrolki walidujące – dostępne są wszystkie kontrolki walidujące dostępne w normalnym ASP.NET

Brak obsługi cookie

Większość urządzeń mobilnych niestety nie obsługuje plików cookie przez co mogą wystąpić problemy między innymi z sesją oraz uwierzytelnianiem, gdzie domyślnie te mechanizmy korzystają z plików cookie (np. do przechowywania ID sesji). Dlatego w przypadku sesji należy ustawić właściwość cookieless na true, dzięki czemu ID sesji będzie przekazywany w adresie url. Aby to zrobić trzeba w web.configu (dla system.web) dodać:

   1: <sessionState cookieless="true" />

Adaptacyjny rendering

Na rynku istnieją różnego rodzaju urządzenia mobilne, które czasami mają bardzo ograniczone możliwości wyświetlania strony. Dlatego kontrolki mobilne ASP.NET umożliwiają renderowanie strony w zależności od przeglądarki, z której jest wysłane żądanie do serwera. I tak na SmartPhonie kontrolka kalendarza może być wyświetlona w sposób identyczny, jak w normalnej wersji strony w przeglądarce na komputerze, co widać na rysunku niżej:

imageNatomiast w prostym telefonie wybór daty z kalendarza może przebiegać w kilku etapach, co widać na poniższym rysunku:

image 

Gdzie wybór daty może przebiegać w trzech krokach:

  • wybór miesiąca np. wrzesień 2006
  • wybór tygodniach np. 10 – 16 września
  • wybór dnia tygodnia środa 12 września

W każdym żądaniu HTTP jest przekazywany nagłówek User-Agent, w którym znajduje się ciąg znaków identyfikujący przeglądarkę z jakiej korzysta użytkownik. ASP.NET przechowuje w plikach o rozszerzeniu browser konfigurację sposobu wyświetlania strony w zależności od przeglądarki. Globalnie pliki te przechowywane są w katalogu c:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\Browsers\. Właśnie na podstawie tych plików ASP.NET decyduje w jaki sposób ma zostać wyświetlona strona, czy to kontrolki (np. Calendar pokazany wyżej).

Dodatkowo programista z poziomu kodu behind może sprawdzić, czy żądanie przyszło z urządzenia mobilnego, czy nie. Na podstawie tej informacji może wykonać odpowiedni kod. Aby to sprawdzić, wystarczy sprawdzić wartość właściwości Request.Browser.IsMobileDevice.

   1: if(Request.Browser.IsMobileDevice)
   2: {
   3:     //kod dla mobilnej wersji   
   4: }else
   5: {
   6:     //kod dla wersji normalnej
   7: }

Tagi: , , , ,