Web Client Software Factory udostępnia bardzo ciekawy i przydatny mechanizm komunikacji ze stanem przechowywanym w sesji. W poniższym przykładzie podczas tworzenia obiektu do pola zostanie wstrzyknięta odpowiednia wartość pobrana z sesji:
1: public class MyClass 2: { 3: [SessionStateKey("MyNumber")] 4: public StateValue<int> MyNumber;
Do wartości tej dostać się można następująco:
1: int number = MyNumber.Value;
Wszystko za sprawą Object Buildera. Jakie korzyści płyną z zastosowania takiego rozwiązania? Oprócz ustandaryzowanego i prostego sposobu wykorzystania sesji najważniejsza jest możliwość przeprowadzenia testów jednostkowych na obiektach polegających na wartościach pobieranych z sesji.
Ale ja nie do końca o tym chciałem… Kilka tygodni temu Kuba Binkowski w swoim wystąpieniu na wg.net pokazał podobne rozwiązanie w Unity, tyle że pobierające wartość z URL. No i właśnie coś takiego dodamy za chwilę do WCSF.
Cały proces rozpoczyna się od odpalenia Reflectora i analizy szczegółów implementacyjnych StateValue, ponieważ funkcjonalność będzie praktycznie ta sama. A potem – sam miód, czyli implementacja. Zatem po kolei, do dzieła!
1. Pierwszy krok to utworzenie interfejsu analogicznego do IStateValue. Interfejs ten zdefiniuje nam kontrakt komunikacyjny pomiędzy naszym systemem a URLem i będzie prawie taki sam jak wspomniane IStateValue. Jedyna różnica to typ zwracany przez właściwość Value. W naszym przypadku będzie to string, ponieważ to właśnie możemy z URLa wyciągnąć. Dodatkowo należy zwrócić uwagę na właściwość Request – nie wołamy bezpośrednio HttpContext.Current.Request. Zamiast tego uzupełnimy tą wartość później, korzystając z innych mechanizmów dostępnych w WCSF.
1: public interface IQueryStringValue 2: { 3: string KeyName { get; set; } 4: string Value { get; } 5: HttpRequest Request { get; set; } 6: }
2. Następnie wypada interfejs ów zaimplementować. Tworzona klasa będzie wykorzystywać typ ogólny, dzięki czemu uzyskamy możliwość konwersji z ciągu znaków do liczby czy daty bez ingerencji końcowego programisty.
1: public class QueryStringValue<T> : IQueryStringValue 2: { 3: public string KeyName { get; set; } 4: 5: public HttpRequest Request { get; set; } 6: 7: string IQueryStringValue.Value 8: { 9: get { return Request.QueryString[KeyName]; } 10: } 11: 12: public T Value 13: { 14: get 15: { 16: var v = ((IQueryStringValue)this).Value; 17: 18: if (string.IsNullOrEmpty(v)) 19: return default(T); 20: 21: return (T)Convert.ChangeType(v, T); 22: } 23: } 24: }
3. Mamy już strukturę potrzebną do przechowywania danych pobranych z QueryStringa. Zauważmy jednak, że brakuje jeszcze odpowiednika atrybutu SessionStateKey będącego znakiem dla ObjectBuildera że w tym miejscu należy się zatrzymać i “coś zrobić”. Implementacja takiego oznaczenia jest banalnie prosta, ponieważ tak naprawdę jedyne czego potrzebujemy to klucz pod którym należy szukać żądanej wartości:
1: [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] 2: public sealed class QueryStringKeyAttribute : Attribute 3: { 4: public string QueryStringKey { get; private set; } 5: 6: public QueryStringKeyAttribute(string queryStringKey) 7: { 8: QueryStringKey = queryStringKey; 9: } 10: }
4. Zatrzymajmy się na chwilę i spójrzmy co już napisaliśmy. Cała instrastruktura konieczna do wykorzystania mechanizmu jest gotowa. W akcji będzie to wyglądać tak:
1: [QueryStringkey("MyNumber")] 2: private QueryStringValue<int> SomeNumber;
…przy czym obsługiwany URL to http://www.xxx.com/yyy.aspx?MyNumber=666.
Ale to oczywiście nie koniec. Nigdzie jeszcze fizycznie nie dobieramy się do adresu, nigdzie nie wciskamy się w proces tworzenia obiektu przez ObjectBuilder! Kroczmy zatem dalej ścieżką prawych i sprawiedliwych. Jesteśmy blisko!
5. Teraz czeka nas najtrudniejsze zadanie – musimy napisać własną strategię ObjectBuildera, która powypełnia wszystkie pola oznaczone tym ślicznym atrybutem. Bez kilkukrotnego zerknięcia do Reflectora na implementację SessionStateBindingStrategy się nie obejdzie, ale w końcu po coś to narzędzie mamy, prawda? Tak więc wykonujemy wszystkie konieczne czynności, które ot tak wymienię jedna po drugiej:
- za pomocą WCSFowego pojemnika IoC uzyskujemy instancję usługi, która dostarczy nam aktualny kontekst HTTP (a więc i obiekt HttpRequest)
- przejeżdżamy się po wszystkich polach tworzonego obiektu pomijając te mające inny typ niż implementację naszego interfejsu IQueryStringValue
- z deklaracji w/w pól pobieramy instancję atrybutu QueryStringKey zawierającą klucz wskazujący na żądaną wartość w URLu (poniższa implementacja wyrzuci wyjątek, gdy pole takiego typu nie zostanie oznaczone takim atrybutem)
- wstawiamy w owo pole nowa instancję pożądanego typu, wypełniając jego właściwości przechowujące Key oraz Request
W tym momencie mamy już pole, którego właściwość Value zwróci nam to czego oczekujemy!
1: public class QueryStringBindingStrategy : BuilderStrategy 2: { 3: public override object BuildUp(IBuilderContext context, System.Type typeToBuild, object existing, string idToBuild) 4: { 5: IHttpContextLocatorService service = context.Locator.Get<IHttpContextLocatorService>(new DependencyResolutionLocatorKey(typeof(IHttpContextLocatorService), null)); 6: 7: if (service != null) 8: { 9: var httpContext = service.GetCurrentContext(); 10: foreach (var field in typeToBuild.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) 11: { 12: if (typeof(IQueryStringValue).IsAssignableFrom(field.FieldType) == false) 13: continue; 14: 15: IQueryStringValue queryStringValue = (IQueryStringValue)Activator.CreateInstance(field.FieldType); 16: QueryStringKeyAttribute attribute = (QueryStringKeyAttribute)field.GetCustomAttributes(typeof(QueryStringKeyAttribute), false)[0]; 17: 18: queryStringValue.KeyName = attribute.QueryStringKey; 19: queryStringValue.Request = httpContext.Request; 20: field.SetValue(existing, queryStringValue); 21: } 22: } 23: 24: return base.BuildUp(context, typeToBuild, existing, idToBuild); 25: } 26: }
Jeszcze jedna mała uwaga: powyższa implementacja pozwala na potraktowanie w ten sposób WSZYSTKICH, także prywatnych, pól. Dostępny w WCSF mechanizm można zastosować jedynie do pól publicznych. Powód? Linijka numer 10. Tutaj jawnie żądamy dostępu do pól publicznych i niepublicznych, podczas gdy implementacja strategii StateValue wykorzystuje bezparametryczną wersję metody GetFields() zwracającą jedynie publiczne pola.
6. Został ostatni kroczek. Musimy powiedzieć ObjectBuilderowi że mamy o taki cudny mechanizm, który chcielibyśmy w proces tworzenia obiektów wprząc. Reflector w kilka chwil pokazuje nam w którą stronę gęby nasze należy zwrócić i co uczynić, aby było to możliwe. Rozwiązaniem jest własna klasa dziedzicząca z WebClientApplication nadpisująca jedną metodę:
1: public class CustomWebApplication : WebClientApplication 2: { 3: protected override void AddBuilderStrategies(Microsoft.Practices.ObjectBuilder.IBuilder<WCSFBuilderStage> builder) 4: { 5: base.AddBuilderStrategies(builder); 6: 7: builder.Strategies.AddNew<QueryStringBindingStrategy>(WCSFBuilderStage.Initialization); 8: } 9: }
A co dalej z tą klasą zrobić – było ostatnio.
That’s all folks!
Żeby nie było tak słodko dodam, że całą tą pracę wykonałem właściwie na marne. Po napisaniu owego rozwiązania wpisałem z ciekawości w Google “WCSF QueryStringValue” i… co się okazało? Istnieje sobie projekcik WCSF Contrib i tam dokładnie takie samo rozwiązanie siedzi już od jakiegoś czasu. No ale, co się człowiek nauczy to jego.
Jest też jeszcze jedna sprawa. W WCSF możemy wykorzystywać sesję (jak to brzmi…) również w inny sposób. Istnieje sobie atrybut StateDependency, dzięki któremu możemy podobne sztuczki robić z parametrami metod! To jednak dużo wyższa szkoła jazdy – bez zagłębienia się po uszy w kod źródłowy WCSF i jego modyfikacji się nie obejdzie. Zatem odpowiednik, czyli QueryStringDependency (którego już w WCSF Contrib nie uświadczymy, co jest absolutnie zrozumiałe jeśli się poprzednie zdanie jeszcze raz przeczyta), stworzymy sobie może innym razem.