Podczas korzystania z WCF najprostszą drogą do wywołania metody udostępnianej przez jakąś usługę jest pozwolenie Visual Studio na wygenerowanie odpowiedniego proxy, stworzenie jego instancji... i już - mamy metody usługi do dyspozycji. Bez wysiłku, bez kodu... bez sensu?
O tym, dlaczego takie podejście NIE jest wyborem słusznym, rozpisywać się nie będę. Zainteresowanych argumentami "przeciw" odsyłam do fajnego artykułu autorstwa Miguela Castro na Code-Magazine: "WCF the Manual Way… the Right Way". Propozycji alternatyw jest w internecie sporo i mają dużą wartość edukacyjną. Jednak gdy przyszło do prawdziwej zabawy z WCF, rozszerzyłem trochę te rozwiązania. Całość kodu wykorzystującego usługi WCF rozbiłem na dwie części: właściwe Proxy oraz klasę udostępniającą odpowiednie Proxy wedle naszego zapotrzebowania.
Pierwsza część, czyli klasa Proxy eliminująca kod generowany przez Visual Studio, przedstawia się następująco:
1: public class ServiceProxy<T> : ClientBase<T>, IDisposable where T : class
2: {
3:
4: public ServiceProxy()
5: {
6: }
7:
8:
9: public ServiceProxy(string endpointConfigurationName)
10: : base(endpointConfigurationName)
11: {
12: }
13:
14: public T GetChannel()
15: {
16: return base.Channel;
17: }
18:
19: public void Dispose()
20: {
21: try
22: {
23: if (base.Channel != null)
24: {
25: if (base.State != CommunicationState.Faulted)
26: {
27: base.Close();
28: }
29: else
30: {
31: base.Abort();
32: }
33: }
34: }
35: catch (CommunicationException)
36: {
37: base.Abort();
38: }
39: catch (TimeoutException)
40: {
41: base.Abort();
42: }
43: catch (Exception)
44: {
45: base.Abort();
46: throw;
47: }
48: }
49: }
Wielkiego odkrycia nie ma tu żadnego. Klasa ta ma właściwie dwa zadania: dać nam dostęp do kanału implementującego komunikację z żądanym serwisem oraz odpowiednią obsługę Dispose(). O problemach z Dispose() można poczytać na MSDN w artykule "Avoiding Problems with the Using Statement".
Wprowadziłem dość istotną modyfikację w stosunku do fruwających po necie przykładów: ograniczyłem liczbę konstruktorów. Moja klasa Proxy udostępnia tylko dwa konstruktory - domyślny oraz przyjmujący nazwę wpisu z konfiguracji. Powód jest bardzo prosty, choć niekoniecznie każdy musi o takim fakcie wiedzieć: te konstruktory umożliwiają cache'owanie przez WCF raz utworzonych obiektów ChannelFactory. Dzięki temu dalsze instancjonowanie samych Proxy jest bardzo lekkim procesem. Więcej o tym na blogu Wenlong Dong: "Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices".
Przedstawiona powyżej klasa może znajdować się w jakimś współdzielonym assembly, dostępna dla każdej aplikacji klienckiej.
Ciągłe pisanie takiego kodu nie do końca mi się jednak podobało:
1: using (var proxy = new ServiceProxy<IMyService>())
2: {
3: proxy.GetChannel().MyMethod();
4: }
Dlatego też bezpośrednio w aplikacji klienckiej umieszczam małego helpera, który pozwala skrócić te instrukcje do takich wywołań:
1: ServiceProxyProvider<IMyService>.Invoke(x => x.MyAction());
2: string result = ServiceProxyProvider<IMyService>.Invoke(x => x.MyFunction());
Oprócz oczywistej korzyści, jaką jest mniejsza ilość kodu (choć można by dyskutować czy w tym konkretnym przypadku to faktycznie tak wielka korzyść), otrzymujemy jeszcze jeden bonus: możemy w jednym miejscu zarządzać każdym wywołaniem zdalnej usługi. Błyskawicznie przychodzącym na myśl wykorzystaniem tej zalety jest wrzucenie tu ustawiania parametrów uwierzytelniania (jakie fancy określenie na login i hasło:) ). W poniższej implementacji, pochodzącej z aplikacji WinForms, zrobiłem jednak coś innego:
1: public class ServiceProxyProvider<TService> where TService : class
2: {
3: public static void Invoke(Action<TService> operation)
4: {
5: using (var proxy = new ServiceProxy<TService>())
6: {
7: WaitingCursor(() =>
8: operation(proxy.GetChannel())
9: );
10: }
11: }
12:
13: public static TResult Invoke<TResult>(Func<TService, TResult> operation)
14: {
15: TResult result = default(TResult);
16:
17: Invoke(x =>
18: {
19: result = operation(x);
20: });
21:
22: return result;
23: }
24:
25: private static void WaitingCursor(Action operation)
26: {
27: Form activeForm = Form.ActiveForm;
28:
29: if (activeForm != null)
30: {
31: activeForm.Cursor = Cursors.WaitCursor;
32: }
33:
34: try
35: {
36: operation();
37: }
38: finally
39: {
40: if (activeForm != null)
41: activeForm.Cursor = Cursors.Default;
42: }
43: }
44: }
Każde wywołanie skutkuje zmianą kursora na klepsydrę bądź Viściano-Siódemkowe Błękitne Koło Zagłady, dzięki czemu użytkownik wie, że "coś się dzieje i ma być cierpliwy".
Macie jakieś ciekawe doświadczenia w tym zakresie? Uwagi, sugestie, dotyczące przedstawionego rozwiązania?
Wkrótce powinien pojawić się dłuższy post traktujący o WCF, gdzie na żywym zarodku aplikacji będzie można zobaczyć to w praktyce. W tymczasem - borem lasem!