Skocz do zawartości

wiesniak@Blog.FA

  • wpisy
    12
  • komentarzy
    134
  • wyświetleń
    40067

Tworzymy stronę www w ASP.NET cz.1,5 - interfejs Silverlight (Akt 1)


wies.niak

1895 wyświetleń

Hejo!

Dzisiejszy wpis nie będzie bezpośrednio dotyczył ASP.NET i dlatego w nazwie ma 1.5. W jednym z komentarzy do poprzedniej części Lord Hrabula zasugerował stworzenie wersji Księgarni CDA w technologii Silverlight. Zgodnie z życzeniem, stwórzmy naszą księgarnię w SL.

Całość podzielona jest na trzy wpisy. Wynika to z wprowadzenia kolorowania składni, które to kolorowanie niestety zjada dużo miejsca.

Kod źródłowy w całości znajduje się na końcu Aktu trzeciego.

W poprzednim wpisie poświęciłem trochę uwagi rozbiciu całej aplikacji na kilka projektów cząstkowych. Nie miałem w planach dodawania innego interfejsu użytkownika, ale teraz widać, że się to opłaciło ? nie będę musiał pisać warstwy dostępu do danych. Nasza aplikacja jest bardzo prosta i posiada trzy warstwy. Nie pisałem o tym wcześniej, ale gdyby aplikacja miała być bardziej złożona, dołożyłbym jeszcze jedną warstwę pomiędzy prezentery, a DAL. Nazwałbym tę warstwę warstwą biznesową (BAL ? business application layer) i umieścił w niej logikę aplikacji. Dzięki temu posunięciu, dodając kolejny interfejs, tak jak teraz, nie musiałbym kopiować / przepisywać dużych ilości kodu. Dla porównania polecam spojrzeć do kodu aplikacji OpinieCDA (2 wpisy temu). Aplikacja ASP.NET posiada dużą część logiki w code-behind i pisząc GUI w WPF był to wrzód na tyłku, bo musiałem to przepisywać i przerabiać kod aby dostosować go pod nowy interfejs.

Silverlight jest częścią WPF przeznaczoną do tworzenia stron internetowych. Pisząc w SL trzeba pamiętać o paru rzeczach. Aplikacja SL jest pobierana w całości na komputer oglądającego. Podczas akcji użytkownika w ASP.NET następuje postback, czyli wysłanie informacji o zdarzeniu do serwera, gdzie znajduje się cały kod naszej aplikacji. W SL nie mamy takiego ?luksusu? i musimy sami obsłużyć nawiązywanie połączeń z serwerem. Aby było to możliwe, serwer musi udostępnić serwis, z którym nasza aplikacja będzie mogła się połączyć. Wiąże się z tym sposób obsługi połączeń. Mianowicie jest on asynchroniczny. Oznacza to, że wywołując jakąś metodę serwisu, nie dostaniemy od razu odpowiedzi jako wynik z metody. Zamiast tego, po otrzymaniu odpowiedzi, zostanie wywołana metoda callbackowa, którą przypięliśmy do zdarzenia informującego o odpowiedzi. Brzmi może skomplikowanie, ale później zobaczymy, że jest to całkiem proste.

Serwis możemy zrealizować na jeden z dwóch sposobów. Pierwszy to stary, dobry serwis asmx z .NET 1.1 (o ile mnie pamięć nie myli). Drugi sposób to użycie WCF ? Windows Communication Foundation przedstawionego w .NET 3.0. Mam raczej małe doświadczenie z WCF, ale raz próbowałem stworzyć serwis WCF na potrzeby aplikacji SL działającej w środowisku Sharepoint 2010. Był to mały koszmar ze względu na uprawnienia, jakoś się jednak udało. Po paru dniach wszystko się totalnie posypało i nie dało się tego naprawić (może byłem zbyt głupi) ? skończyło się na powrocie do asmx :-) No ale żeby nie było, że się uwsteczniamy, spróbuję stworzyć naszą aplikację przy użyciu WCF.

Klasycznie użycie serwisu polega na dodaniu referencji w projekcie, który ma tego serwisu używać. Każda zmiana w serwisie pociąga konieczność aktualizacji referencji. Istnieje też coś takiego jak WCF RIA Services. Jest to dodatek, którego zadaniem jest przybliżyć aplikację SL z częścią serwerową poprzez generowanie kodu referencji bez potrzeby łączenia z serwisem oraz aktualizacji przy zmianach. Aby nie pchać się na obce wody, użyję standardowego rozwiązania z referencją do serwisu.

Skoro już mamy nakreślone tło, weźmy się za pisanie kodu.

Aby nie mieszać w istniejącym kodzie stwórzmy projekt na nasz serwis WCFowy. Z menu dodajemy projekt ?WCF Service Application? i nazywamy go ?KsiegarniaCDA.WCFService?. W Solution Explorerze doda nam się:

blogentry-7427-1331519800.png

Zmieńmy nazwy klasy oraz interfejsu z Service1 na Service. Teraz do projektu dodajmy referencje do już istniejących projektów DAL oraz DTO. Gdybyśmy dysponowali warstwą logiki biznesowej (którą mógłbym trochę na siłę wprowadzić i umieścić w niej np. sortowanie wyników wyszukiwań), nasz serwis referowałby właśnie do warstwy biznesowej aby była ona wspólna dla różnych interfejsów użytkownika.

Zróbmy na początek porządek w klasie serwisu. Otwieramy plik Service.svc.cs i usuwamy z klasy Service przykładowe metody. Przechodzimy do interfejsu. Zanim usuniemy bieżącą zawartość interfejsu zwróćmy uwagę na jedną rzecz, a mianowicie na atrybuty.

Przede wszystkim cały interfejs jest opisany atrybutem

CODE
[ServiceContract]

Określa on, że dany interfejs albo klasa definiuje kontrakt serwisu.

Drugi atrybut to

CODE
[OperationContract]

Dzięki niemu wskazujemy metody, które będą udostępnione przez serwis.

CODE
[DataContract]

Tego atrybutu używamy przy opisywaniu klas, które będą przechodzić przez serwis. Określa on, że dana klasa jest serializowalna przez serializer wykorzystywany właśnie przy przesyłaniu danych w WCF.

Ostatni atrybut to

CODE
[DataMember]

Którym opisujemy serializowalne właściwości w klasach przesyłanych przez serwis.

Już wiemy co i jak, więc usuwamy przykładową klasę oraz deklaracje metod z interfejsu.

Serwis jest gotowy do uzupełnienia, więc tworzymy w nim metody wywołujące odpowiednie metody z DAL. Aby nie tworzyć w każdej metodzie nowego obiektu DataManagera strony wyszukiwania książek, zrobimy prywatne pole i zainicjalizujemy je w konstruktorze serwisu. Oto nasza klasa Service

CODE
public class Service : IService

{

#region Fields

private BooksSearchDataManager _booksSearchDataManager = null;

#endregion

#region Constructors

public Service()

{

_booksSearchDataManager = new BooksSearchDataManager();

}

#endregion

#region Public methods

public IEnumerable<Item> AutorzyComboPobierz()

{

return _booksSearchDataManager.AutorzyComboPobierz();

}

public IEnumerable<Item> GatunkiComboPobierz()

{

return _booksSearchDataManager.GatunkiComboPobierz();

}

public IEnumerable<BooksSearchSearchResult> SearchBooks(BooksSearchSearchCriteria searchCriteria)

{

return _booksSearchDataManager.SearchBooks(searchCriteria);

}

#endregion

}

oraz interfejs, który implementuje:

CODE
[ServiceContract]

public interface IService

{

#region Public methods

[OperationContract]

IEnumerable<Item> AutorzyComboPobierz();

[OperationContract]

IEnumerable<Item> GatunkiComboPobierz();

[OperationContract]

IEnumerable<BooksSearchSearchResult> SearchBooks(BooksSearchSearchCriteria searchCriteria);

#endregion

}

Ta da! Serwis w zasadzie gotowy. Aby wszystko ładnie działało, musimy dodać odpowiednie atrybuty do klas DTO. Chwilowo jednak to pominę i zobaczymy co się stanie, gdy o tych atrybutach zapomnimy. Gdybym zapomniał o atrybutach metod, to te metody po prostu były by niewidoczne dla aplikacji korzystających z serwisu.

Trochę może nam się nie podobać, że interfejs jest obok klasy, a nie tak jak ona, wpięty pod plik svc. Visual Studio nie pozwala niestety tego zmienić. Szczęśliwie da się to obejść. Klikamy prawym klawiszem myszy na nasz projekt i wybieramy ?Unload Project?, po czym klikamy jeszcze raz i wybieramy edycję pliku csproj. Znajdujemy linijkę

CODE
<Compile Include="IService.cs" />

Po czym dodajemy element ?DependentUpon?:

CODE
<Compile Include="IService.cs">

  <DependentUpon>Service.svc</DependentUpon>

</Compile>

Aby wszystko ładnie wyglądało zmieniamy jeszcze nazwę pliku na Service.svc.interface.cs (w csproj oraz oczywiście na dysku).

Zamykamy plik projektu po czym wykonujemy ?Reload Project? (PPM na wyładowanym projekcie). W wyniku dostajemy

blogentry-7427-1331519811.png

Katalog App_Data można przy okazji usunąć ? jest on nam zbędny. Ponieważ zmienialiśmy na początku nazwę klasy serwisu, musimy jeszcze otworzyć markup pliku svc (opcja view markup z menu kontekstowego w Solution Explorerze) i poprawić w nim odniesienie do klasy serwisu. Poprawnie wygląda to tak:

CODE
<%@ ServiceHost Language="C#" Debug="true" Service="KsiegarniaCDA.WCFService.Service" CodeBehind="Service.svc.cs" %>

Warto zwrócić uwagę, że właściwość Service przechowuje pełną ścieżkę, łącznie z nazwą biblioteki. Co za tym idzie klasa serwisu może znajdować się w dowolnym miejscu solucji.

Teraz klikamy prawym przyciskiem myszy na plik svc, i wybieramy View in Browser. Jeśli wszystko poszło zgodnie z planem, zobaczymy w przeglądarce stronę naszego serwisu.

blogentry-7427-1331519822.png

Skoro serwis gotowy, zajmijmy się naszą aplikacją silverlightową. Zabierając się za stworzenie projektu SL dość długo zastanawiałem się, jak do niego podejść. Rozważałem, czy wrzucić ?gołą? aplikację, czy pokusić się o użycie frameworka. Ponieważ chcę projekt zrealizować podobnie do aplikacji ASP.NET, wprowadzę wzorzec MVVM i dla ułatwienia od razu skorzystam z toolkitu MVVM Light.

MVVM, czyli Model-View-ViewModel to nic innego jak silverlightowa odmiana MVC i ASP.NETowego MVP. Główna różnica między MVVM a MVP polega na tym, że Silverlight wspiera luźne wiązanie (loose coupling). Pozwala ono na powiązanie widoku z ViewModelem poprzez podane w widoku nazwy właściwości z ViewModelu. Później zobaczymy, że to jest całkiem proste. Gdyby ktoś chciał zgłębić temat, mogę polecić książeczkę ?Pro WPF and Silverlight MVVM. Effective Application Development with Model-View-ViewModel?, która dość przystępnie I zwięźle opisuje mechanizmy bindowania. Warto powiedzieć o jeszcze jednej sprawie. Mianowicie MVVM nie jest idealny. A może to sam Silverlight nie jest? W każdym razie są sytuacje, kiedy nie da się wykonać w prosty sposób powiązania kontrolki z ViewModelem. Odnosi się to do większości zdarzeń w kontrolkach. Można to rozwiązać na dwa sposoby. Pierwszy to stworzenie metody obsługi zdarzenia w code-behind kontrolki, a następnie wywołanie odpowiedniej metody z ViewModelu. Drugi to użycie zachowań (behaviors), które pozwolą powiązać zdarzenie z komendą we ViewModelu. To jest jednak rzecz, która nie dotyczy nas w tym wpisie, więc nie zawracajmy sobie więcej głowy nią.

Jak wspomniałem wcześniej, wesprzemy się odrobinę Toolkitem MVVM Light (http://galasoft.ch/mvvm/). Ponieważ akurat zainstalowałem wersję V4 beta 1 z obsługą SL 5, z takiego szablonu właśnie skorzystam na początek.

Dodajemy do solucji nowy projekt z szablonu ?MvvmLight (SL 5)? i nazywamy go ?KsiegarniaCDA.GUI.SL?. Ponieważ szablon dodał nam parę rzeczy, przejrzyjmy je i usuńmy to, co zbędne. Prawdę mówiąc jest do dla mnie pierwszy głębszy kontakt z tym szablonem dla SL5, więc też będę się go dopiero uczył :-)

blogentry-7427-1331519835.png

Przede wszystkim w App.xaml pojawiła się definicja zasobu.

CODE
<Application.Resources>

    <!--Global View Model Locator-->

    <vm:ViewModelLocator x:Key="Locator"

                         d:IsDataSource="True" />

</Application.Resources>

ViewModelLocator to klasa, której zadaniem jest uproszczenie dostępu oraz bindowanie DataContextu do obiektów ViewModel. Właściwość IsDataSource określa, czy nasze ViewModele będą powiązane z obiektem DataSource, który z kolei ma nam ułatwić dostęp do danych ? o tym za chwilkę. Naszą klasę ViewModelLocator znajdziemy w katalogu ViewModel.

CODE
public class ViewModelLocator

{

static ViewModelLocator()

{

ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

if (ViewModelBase.IsInDesignModeStatic)

{

SimpleIoc.Default.Register<IDataService, Design.DesignDataService>();

}

else

{

SimpleIoc.Default.Register<IDataServiceDataService>();

}

SimpleIoc.Default.Register<MainViewModel>();

}

/// <summary>

/// Gets the Main property.

/// </summary>

[system.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",

"CA1822:MarkMembersAsStatic",

Justification = "This non-static member is needed for data binding purposes.")]

public MainViewModel Main

{

get

{

return ServiceLocator.Current.GetInstance<MainViewModel>();

}

}

/// <summary>

/// Cleans up all the resources.

/// </summary>

public static void Cleanup()

{

}

}

W statycznym konstruktorze konfigurowany jest ServiceLocator. Jest to implementacja wzorca projektowego, którego zadaniem jest ujednolicenie oraz uproszczenie dostępu, w naszym przypadku do obiektów ViewModel. Widać to doskonale kawałek niżej, we właściwości zwracającej ViewModel dla kontrolki MainPage. Oczywiście można zamiast tego dodać prywatny obiekt klasy, utworzyć ko w konstruktorze i zwracać we właściwości (tak było w starszych wersjach szablonu), ale ServiceLocator nam to upraszcza.

Nasz ServiceLocator jest ustawiany obiektem z klasy SimpleIoc, która pochodzi z MVVM Light. IoC to akronim od Inversion of Control. Długo by opowiadać, bo jest to kontener IoC to kolejny wzorzec projektowy, którego celem jest oddzielenie użycia obiektów od ich implementacji. Mówiąc prościej, zamiast posługiwać się konkretną implementacją, używamy interfejsu. Przykład z życia: bardzo popularna biblioteka do logowania komunikatów log4net. Korzystając z niej operuje się za pomocą interfejsu ILogger, natomiast w zależności od konfiguracji, biblioteka zapewnia, że logujemy komunikaty albo do pliku, albo do bazy danych, albo na ekran itd. ? od strony użycia zawsze jest identycznie. Dodam, że mam zamysł zaimplementować coś takiego za jakiś czas jako model dostępu do danych w naszej aplikacji ? będziemy korzystać z inerfejsu, a tylko konfiguracja będzie nam mówić, czy ciągniemy dane z fikcyjnych kolekcji w klasie, czy może z fizycznego pliku bazy danych.

Wróćmy do naszego kodu. SimpleIoC daje nam jedną rzecz, a mianowicie wstrzykuje on do naszych ViewModeli obiekt implementujący interfejs IDataSource. Zaraz zobaczymy to w klasie MainViewModel.

Ostatnia rzecz w konstruktorze klasy ViewModelLocator, o której nie wspomniałem to właściwość ViewModelBase.IsInDesignModeStatic. Jest ona w klasie ViewModelBase, która jest klasą bazową dla ViewModeli (cpt. Obvious, co?). Zadaniem tej właściwości jest rozróżnić, czy w bieżącej chwili aplikacja jest normalnie uruchomiona, czy wygląd jest renderowany przez designer środowiska programistycznego. Pozwala to nam uniknąć sytuacji, w których designer nie potrafi wyrenderować wyglądu kontrolki ze względu na rzeczy, które dzieją się w konstruktorze, a które działają przy normalnym uruchomieniu aplikacji. We ViewModelLocator zmienna ta rozróżnia, jaką implementację IDataSource wybierzemy do wstrzykiwania w nasze ViewModele. Ponieważ nie zamierzam bawić się w generowanie dodatkowego obiektu DataSource dla designera, od razu wyrzucę tę rejestrację:

CODE
static ViewModelLocator()

{

ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

if (!ViewModelBase.IsInDesignModeStatic)

{

SimpleIoc.Default.Register<IDataServiceDataService>();

}

SimpleIoc.Default.Register<MainViewModel>();

}

Rzućmy teraz szybko okiem do MainPage.xaml. Jak widzimy, jest to UserControl. Jedną z ustawianych właściwości kontrolki jest DataContext.

CODE
DataContext="{Binding Main, Source={StaticResource Locator}}"

Jak widać, jako kontekst danych naszej kontrolki przypisany zostaje obiekt z właściwości Main zasobu statycznego Locator. Przypomnę, że zasób ten został zdefiniowany w App.xaml jako zasób aplikacji (dzięki czemu jest dostępny dla wszystkich kontrolek). Jeśli nie chcemy z jakiegoś powodu wiązać ViewModelu z widokiem w xamlu (bo np. chcemy przekazywać specyficzne parametry), zawsze możemy zrobić to w code-behind, np. w konstruktorze. Inna sprawa, że przekazać parametry możemy za pomocą bardzo wygodnego systemu rozsyłania komunikatów dostępnego w MVVM Light, ale to opowieść na cały osobny wpis (przykład użycia znaleźć można w OpinieCDA).

Drugi element kontrolki MainPage to definicja zasobów kontrolki:

CODE
<UserControl.Resources>

    <ResourceDictionary>

        <ResourceDictionary.MergedDictionaries>

            <ResourceDictionary Source="Skins/MainSkin.xaml" />

        </ResourceDictionary.MergedDictionaries>

    </ResourceDictionary>

</UserControl.Resources>

Jak widać, mamy tu zdefiniowany jedynie słownik zasobów, a w nim wskazanie na plik MainSkin.xaml, który pozwala określić wygląd kontrolki. Plik jest pusty (poza znacznikami początku i końca), więc go pominę. Nic też nie stoi na przeszkodzie by w ramach UserControl.Resources zdefiniować style. W ogóle style w SL to bardzo szeroki temat ? można z nimi zdziałać cuda. Przykładowo można całkowicie zmienić wygląd wbudowanych kontrolek po prostu nadpisując ich styl. W Windows Forms nie jest to takie proste (nie wiem, czy nawet nie niemożliwe).

Pozostała część naszej kontrolki to definicja wyglądu.

CODE
<Grid x:Name="LayoutRoot">

    <TextBlock FontSize="36"

               FontWeight="Bold"

               Foreground="Purple"

               Text="{Binding WelcomeTitle}"

               VerticalAlignment="Center"

               HorizontalAlignment="Center"

               TextWrapping="Wrap" />

</Grid>

Mamy Grid, czyli siatkę w której możemy definiować wiersze oraz kolumny, choć jak widać, nie jest to konieczne i elementy zawartości możemy po prostu wrzucać do środka. Grid jednak nie zajmuje się aranżacją zawartości, więc wszystko wyląduje na sobie o ile nie określimy marginesów, więc takie użycie nie jest najlepsze. Silverlight posiada kilka kontenerów, które zajmują się rozkładaniem zawartości. Przykładem może być StackPanel, który ustawia elementy jeden po drugim.

Wewnątrz Grida mamy blok tekstowy z określonym wyglądem. Jak widzimy, właściwość Text jest zbindowana z właściwością WelcomeTitle powiązanego ViewModelu.

Z naszego punktu widzenia kontrolka MainPage mogła by się stać stroną BooksSearch. Zrobimy jednak inaczej, a mianowicie MainPage stanie się kontenerem, odpowiednikiem MasterPage?a ASP.NETowego, w którym osadzimy SLową wersję strony BooksSearch. Dlatego też samą kontrolkę zostawię, jednak TextBlock usunę, ponieważ jest zbędny.

Code-begind kontrolki MainPage jest niemal pusty ? posiada tylko konstruktor z metodą inicjalizującą, która to metoda jest generowana automatycznie przez designer.

CODE
public partial class MainPage : UserControl

{

public MainPage()

{

InitializeComponent();

}

}

Obejrzyjmy teraz zawartość klasy MainViewModel. Przede wszystkim klasa dziedziczy po klasie ViewModelBase.

CODE
public class MainViewModel : ViewModelBase

Klasa ta zawiera parę przydatnych rzeczy. Z naszego punktu widzenia istotna jest jedna ? metoda RaisePropertyChanged. Kiedy tworzymy binding, czasami chcemy aby zmiany we ViewModelu propagowały się do widoku. Samo się to nie stanie i dlatego konieczne jest, aby nasz ViewModel implementował interfejs INotifyPropertyChanged. Implementuje go nasza klasa bazowa, a jego implementacja sprowadza się do implementacji zdarzenia PropertyChanged. Metoda RaisePropertyChanged po prostu wywołuje to zdarzenie, jako parametr pobierając nazwę właściwości, która się zmieniła.

Zobaczmy na właściwość WelcomeTitle

CODE
/// <summary>

/// The <see cref=

"WelcomeTitle" /> property's name.

/// </summary>

public const string WelcomeTitlePropertyName = "WelcomeTitle";

private string _welcomeTitle = string.Empty;

/// <summary>

/// Gets the WelcomeTitle property.

/// Changes to that property's value raise the PropertyChanged event. 

/// </summary>

public string WelcomeTitle

{

get

{

return _welcomeTitle;

}

set

{

if (_welcomeTitle == value)

{

return;

}

_welcomeTitle = value;

RaisePropertyChanged(WelcomeTitlePropertyName);

}

}

Była ona zbindowana do usuniętego TextBlocku. Jak widać, w setterze właściwości wołana jest metoda RaisePropertyChanged. Gdyby jej nie było, wartość właściwości została by pobrana przy uruchomieniu aplikacji (przy renderowaniu kontrolki), a jakiekolwiek zmiany właściwości nie przenosiły by się do widoku. Gdybyśmy mieli dwie zakładki, to dopiero zmiana zakładek tam i z powrotem spowodowała by ponowne wyrenderowanie kontrolki i pobranie wartości. Jest to banalna sprawa, ale jeśli się o tym nie wie, potrafi to zdrowo napsuć krwi.

Właściwość omówiliśmy, więc możemy ją usunąć, ponieważ jest nam zbędna. Spójrzmy na konstruktor.

CODE
private readonly IDataService _dataService;

/// <summary>

/// Initializes a new instance of the MainViewModel class.

/// </summary>

public MainViewModel(IDataService dataService)

{

_dataService = dataService;

_dataService.GetData(

(item, error) =>

{

if (error != null)

{

// Report error here

return;

}

WelcomeTitle = item.Title;

});

}

Jak wspominałem wcześniej, jako parametr otrzymuje on obiekt implementujący interfejs IDataService. Przypisujemy go do prywatnego pola, a następnie ?przypinamy się? do metod operujących na danych. Pierwotnie chciałem pozbyć się tego elementu i po prostu odwoływać się do naszego serwisu poprzez referencję, którą za parę chwil dodamy. Postanowiłem jednak, że zamaskujemy nasz serwis za pomocą obiektu DataService. Bo czemu nie? :) Warto zauważyć, że to przypięcie to metod z dataService jest zrobione za pomocą wyrażenia lambda. Jest to dość wygodne, ale moje doświadczenia pokazują, że ma to jedną wadę: jeśli mamy wiele metod z serwisu, do których musimy się odwoływać, to po pewnym czasie dość trudno jest nam znaleźć to właściwe odwołanie. Z tego powodu w naszym przykładzie po prostu będę tworzył prywatne metody. Odwołanie do GetData usuwamy ? nie potrzebujemy go tutaj.

W katalogu Design, w Solution Explorerze znajduje się klasa DesignDataService, która udostępnia wersję serwisu używaną przy renderowaniu interfejsu w designerze. Jest ona nam zbędna, więc wywalamy (ciało identyczne jak w klasie DataService, do której zaraz dojdziemy).

Klasa DataService jest prosta.

CODE
public class DataService : IDataService

{

public void GetData(Action<DataItemException> callback)

{

// Use this to connect to the actual data service

var item = new DataItem("Welcome to MVVM Light");

callback(item, null);

}

}

Implementuje interfejs IDataService i zawiera metodę symulującą pobieranie danych. Jako parametr przyjmuje metodę dwuparametrową, która zostanie zawołana po skończeniu pobierania danych. Nie potrzebujemy metody GetData ? wywalamy ją z klasy oraz interfejsu.

CODE
public interface IDataService

{

}

Klasa DataItem jest prostym obiektem DTO z właściwością Title ? usuwamy niepotrzebną klasę.

Przejrzeliśmy wszystko, więc zajmijmy się dodawaniem naszej strony BooksSearch. Jeszcze szybkie sprawdzenie, czy projekt się buduje. Jest OK, więc jedziemy dalej.

Na początek zadbajmy o źródło danych. Klikamy prawym przyciskiem na projekt silverlightowy i wybieramy ?Add Service Reference?. Rozwijamy listę Discover i wybieramy w niej ?Services in solution?. Visual Studio powinno znaleźć nasz serwis

blogentry-7427-1331519844.png

Jako przestrzeń nazw serwisu podajemy ?KsiegarniaCDAServiceReference? i dajemy ?OK?. Do solucji doda nam się referencja (wszystkie rzeczy widać dzięki opcji ?Show all files? Solution Explorera):

blogentry-7427-1331519859.png

Podepnijmy się do serwisu w klasie DataService. Dodajemy namespace:

CODE
using KsiegarniaCDA.GUI.SL.KsiegarniaCDAServiceReference;

po czym tworzymy prywatne pole przechowujące naszą referencję do serwisu.

CODE
private readonly ServiceClient _serviceClient;

W konstruktorze inicjalizujemy obiekt:

CODE
public DataService()

{

_serviceClient = new ServiceClient();

}

Domyślny konstruktor korzysta z ustawień wygenerowanych automatycznie przy dodawaniu referencji. Ustawienia te zapisane są w pliku ServiceReferences.ClientConfig. Zostawię to u nas tak, jak jest, natomiast w rzeczywistej aplikacji trzeba podać właściwy adres serwisu z zewnątrz (np. poprzez przekazanie adresu strony do aplikacji silverlight w miejscu osadzenia na stronie www). Przy okazji wspomnieć trzeba o kwestii uprawnień przy łączeniu do serwisu. Serwis WCF uruchamiany jest na jakimś losowym porcie ustawionym w konfiguracji projektu. Podobnie ma się sprawa z aplikacją Silverlight, która jest w naszym przypadku osadzana na dynamicznie wygenerowanej stronie, również na losowym porcie. W takim przypadku aplikacja nie będzie w stanie połączyć się z serwisem ze względu odwołanie do innej domeny (dostaniemy Cross Domain error). Aby tego uniknąć, do katalogu z serwisem wrzuciłem dwa pliki crossdomain.xml oraz clientaccesspolicy.xml. Nie będę tu przytaczał ich treści bo nie jest to częścią wpisu ? wystarczy wiedzieć, do czego one służą.

Wracamy do klasy DataService i dodajemy metody zawarte w naszym serwisie. Zaczniemy od AutorzyComboPobierz.

CODE
public void AutorzyComboPobierz(Action<IEnumerable<Item>, Exception> callback)

{

if (callback != null)

{

_serviceClient.AutorzyComboPobierzCompleted += (s, e) => callback(e.Result, e.Error);

_serviceClient.AutorzyComboPobierzAsync();

}

}

Ponieważ nasz serwis działa asynchronicznie, zaczynamy od obsługi zdarzenia uruchamiającego się po zakończeniu wywołania. Wyniki od razu przekazujemy do metody callbackowej podanej jako parametr wywołania. Po rejestracji wykonujemy wywołanie właściwej metody z serwisu. Jak widać, nie przekazujemy żadnych parametrów ponieważ nasza metoda z serwisu faktycznie niczego od nas nie chce. Warto jednak zauważyć, że istnieje możliwość przekazania dowolnego obiektu poprzez parametr UserState przeciążonej wersji metody. Dzięki temu istnieje możliwość przekazania danych między wywołaniem metody a obsługą odpowiedzi. Bardzo dobrym przykładem jest otwieranie okna zapisu pliku po naciśnięciu przez użytkownika przycisku. Często plik trzeba pobrać przez serwis, a więc okno jest potrzebne tak naprawdę dopiero po pobraniu pliku. Problem jednak w tym, że ze względów bezpieczeństwa okno to może być otwarte tylko na żądanie użytkownika, a obsługa callbacku z serwisu już się do tego żadania nie zalicza nawet, jeśli samo wywołanie serwisu miało miejsce po akcji użytkownika. W takim właśnie wypadku przekazujemy obiekt FileSaveDialog przez parametr UserState i po odpowiedzi z serwisu mamy bezproblemowy dostęp do strumienia, w którym będziemy zapisywać plik. Wróćmy jednak do naszego kodu. Dodajemy bliźniaczą metodę GatunkiComboPobierz.

CODE
public void GatunkiComboPobierz(Action<IEnumerable<Item>, Exception> callback)

{

if (callback != null)

{

_serviceClient.GatunkiComboPobierzCompleted += (s, e) => callback(e.Result, e.Error);

_serviceClient.GatunkiComboPobierzAsync();

}

}

Jako ostatnią dodajemy metodę SearchBooks, która poza metodą zwrotną będzie też przyjmowała parametry wyszukiwania.

CODE
public void SearchBooks(Action<IEnumerable<BooksSearchSearchResult>, Exception> callback, BooksSearchSearchCriteria searchCriteria)

{

if (callback != null)

{

_serviceClient.SearchBooksCompleted += (s, e) => callback(e.Result, e.Error);

_serviceClient.SearchBooksAsync(searchCriteria);

}

}

Koniec Aktu pierwszego. Akt drugi tu: klik!

0 komentarzy


Rekomendowane komentarze

Brak komentarzy do wyświetlenia.

Gość
Dodaj komentarz...

×   Wklejony jako tekst z formatowaniem.   Wklej jako zwykły tekst

  Maksymalna ilość emotikon wynosi 75.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...