Samouczek Silverlight – część 3
Praca z danymi
Po uporaniu się z bazowym układem stron i dodaniu kilku kontrolek w części 2, zacznijmy pracować z danymi. Będziemy używać wyszukiwarki Twitter, a w rzeczywistości skorzystamy z API ich usług internetowych (web service API). W naszej aplikacji nie będziemy przechowywać informacji we własnej bazie danych ale chciałbym przedstawić różne sposoby jakie można użyć w Silverlight aby uzyskać dostęp do danych.
Opcje pracy z danymi
Jednym z większych nieporozumień u początkujących z Silverlight jest szukanie bibliotek ADO.NET (pozwalających na bezpośrednie łączenie z bazą danych). Nie szukajcie, nie ma ich tam. Pamiętajcie, Silverlight to technologia kliencka, która jest dostarczana poprzez Internet. Nie chcielibyście aby jakiś dodatek w przeglądarce miał dostęp do waszej bazy danych, musielibyście upublicznić dostęp do niej. Zdajemy sobie sprawę. że tak się po prostu NIE robi.
Logicznym rozwiązaniem jest udostępnianie danych poprzez warstwę usług. Tak właśnie Silverlight komunikuje się z danymi. Oto główne możliwości:
- Usługi internetowe (Web services): SOAP, ASP.NET web services (ASMX), WCF services, POX, REST endpoints
- Gniazda (Sockets): sieciowe gniazda komunikacyjne (dwukierunkowy punkt połączenia)
- Plik (File): korzystanie ze statycznej zawartości poprzez usługi internetowe
Gniazda (Sockets)
To najbardziej skomplikowana metoda dostępu do danych. Wymaga otwarcia gniazda po stronie klienta oraz komunikacji w konkretnym zakresie portów. Jeśli te warunki są akceptowalne, rozwiązanie to, może być bardzo wydajnym sposobem komunikacji w Twojej aplikacji. Nie wydaje mi się aby ta metoda była szeroko używana w aplikacjach internetowych – dzisiaj, gniazda mają zastosowanie w zamkniętych środowiskach biznesowych. Klika dodatkowych informacji o gniazdach (w języku angielskim):
- Dokumentacja SDK
- Praca z danymi używając gniazd – dotyczy Silverlight 2 ale wciąż przydatne aby zrozumieć metodologie
Praca z gniazdami wymaga dogłębnego zbadania środowiska w jakim będzie pracowała aplikacja, zanim podejmiemy decyzję o zaprojektowaniu komunikacji w ten sposób.
Pliki (File access)
- Silverlight może korzystać z danych lokalnych lub dostępnych w Internecie. W przypadku lokalnych danych, aplikacja nie ma bezpośredniego dostępu do plików, raczej komunikuje się z nimi poprzez akcje inicjowane przez użytkownika poprzez dialogi otwarcia/zapisu pliku na lokalnej maszynie.Dodatkowo można użyć zwykłych plików tekstowych lub XML w Internecie, a aplikacja Silverlight używać będzie standardowych komend HTTP do zapisu/odczytu informacji. Kilka dodatkowych informacji w języku angielskim:
- Dialogi otwarcia i wgrania pliku
- Dialog zapisu pliku
Praktycznym zastosowaniem może by zapis konfiguracji lub bardzo małych ilości informacji.
Usługi internetowe (web services)
Główny sposób kontaktu z danymi w Silverlight – poprzez warstwę usługi. Silverlight wspiera komunikację ze standardowymi usługami Internetowymi ASP.NET (ASMX) lub usługami bazującymi na WCF poprzez znany w Visual Studio dialog Add Service Reference, generując niezbędne klasy wraz z walidacją.
Dodatkowo możemy użyć standardów HTTP aby komunikować się z POX (Plain old XML – czysty XML) lub usługami typu REST. Zrozumienie różnych typów usług i możliwości komunikacji z nimi, jest prawdopodobnie najlepiej zainwestowanym czasem edukacji twórcy aplikacji. Wiedza ta jest ogromnie przydatna przy wyborze najlepszych metod pasujących do wymagań aplikacji. Kilka informacji w języku angielskim:
- Usługi WCF i ASP.NET
- Komunikacja z usługami poprzez HTTPS
- Usługi .NET RIA
- Wybór sposobu komunikacji z danymi
W trzecim punkcie omawiane są usługi .NET RIA, nowy model próbujący ułatwić dostęp do danych. Odnośnik prowadzi do wideo omawiającego podstawy modelu. Usługi RIA najlepiej użyć gdy posiadamy bazę danych i usługi dostarczające dane są częścią tej samej aplikacji Internetowej, która udostępnia aplikację Silverlight.
Dostęp asynchroniczny
W Silverlight każdy dostęp do danych jest asynchroniczny. Prawdopodobnie jest to kolejny mechanizm utrudniający zrozumienie tematu przez twórców aplikacji Internetowych. Dla przykładu, w świecie serwerów zobaczymy coś takiego:
MyWebService svc = new MyWebService(); string foo = svc.GetSomeValue(); MyTextBox.Text = foo;
W Silverlight taki synchroniczny dostęp do danych jest niemożliwy. Bez doświadczenia z programowaniem asynchronicznym może być początkowo zagubieni, ale warto poznać tę metodę, wzbogaci wasz warsztat. W Silverlight, odpowiednik powyższego kody wyglądał by tak:
MyWebService svc = new MyWebService(); svc.Completed += new CompletedHandler(OnCompleted); svc.GetSomeValue(); void OnCompleted(object sender, EventArgs args) { MyTextBox.Text = args.Result; }
Zwróćmy uwagę, że używamy rezultatów zapytania w obsłudze zakończonego zdarzenia. Jest to szablon, który będzie się ciągle powtarzał w podstawowej komunikacji z usługami.
Od siebie dodam, że różnicę w synchroniczności, można zrozumieć na przykładzie zakupów. Synchroniczny jest gdy idziemy do sklepu, prosimy sprzedawcę o przedmiot i go od razu dostajemy (akcja i reakcja zgrana w czasie). Asynchroniczny gdy kupujemy w sklepie internetowym, zamawiamy i czekamy, aż dojdzie do nas przesyłka (akcja i oczekiwanie na zakończenie reakcji).
Dostęp do danych z innej domeny
Ponieważ Silverlight jest technologią internetowego klienta, pracuje w środowisku przeglądarki i podlega określonym zasadom dostępu. Jedną z takich zasad jest dostęp do innych domen. Jeśli Twoja aplikacja dostępna jest w jednej domenie, to nie może komunikować się z danymi w innej domenie, chyba, ze usługa na to pozwala. Takie opcjonalne zachowanie jest ogólnie znane jako pliki zasad dostępu do innej domeny (cross-domain policy files). Silverlight, jak każdy tego typu dodatek przeglądarki, wspiera te zasady. Wcześniej czy później, jako twórca w technologii Silverlight, spotkacie się z tą problematyką. Lepiej zapoznać się z nią teraz. Kilka odnośników w języku angielskim:
- Silverlight i pliki zasad dostępu do innej domeny
- Podpowiedzi do plików zasad dostępu do innej domeny
- Narzędzia ułatwiające rozwiązywanie problemów z dostępem do innych domen
- Moja aplikacja Silverlight nie komunikuje się z moja usługą
- Przegląd komunikacji pomiędzy domenami
W naszej aplikacji, będziemy komunikować się z usługą zlokalizowaną w innej domenie, także będziemy musieli spełnić wszelkie zasady dostępu. Usługa wyszukiwania w serwisie Twitter pozwala na dostęp z użyciem ich plików zasad. Inne usługi tego serwisu nie pozwalają na taki dostęp, i nie mógłbyś z nich skorzystać bezpośrednio poprzez Silverlight. W takiej sytuacji należałoby zastąpić odwołania do tych usług, własna usługą, w której mógłbyś umożliwić dostęp do innych domen poprzez pliki zasad w Silverlight. Zakręcone? Jest to prostsze niż na to wygląda.
Powszechne nieporozumienie: Potrzebujesz w swojej usłudze komunikującej się z innymi domenami, Silverlight i Adobe plików zasad dostępu do innych domen. Jest to NIEPRAWDĄ. Często widzę wpisy o treści: mam pliki crossdomain.xml i clientaccesspolicy.xml i dalej dostęp nie działa. Jeśli tworzycie usługę w celu wielo domenowego dostepu w Silverlight, potrzebujecie tylko pliku w formacie clientaccesspolicy.xml – to jest najpierw wyszukiwane, jest to najbardziej elastyczne i bezpieczne rozwiązanie dla Silverlight.
Po tym dość ogólnym wstępie, zacznijmy komunikacje z naszymi danymi.
Komunikacja z API Twitter-a
API wyszukiwarki Twitter-a to prosta usługa typu REST. W naszej aplikacji będziemy używać żądań typu GET. Format zapytań jest przestawiony w specyfikacji ATOM, co ułatwia nasze zadanie, gdyż jest to standard i w Silverlight mamy dostępne biblioteki go wspierające.
Wyślemy zapytanie do API w momencie kliknięcia na przycisk Szukaj oraz gdy zostały wpisane słowa do wyszukiwania. Przygotujmy zdarzenie obsługujące kliknięcie w przycisk, tak jak to zrobiliśmy w części 1. W pliku Search.xaml dodajemy zdarzenie do przycisku SearchButton, funkcje obsługującą zdarzenie nazwiemy SzukajTweeta:
<Button x:Name="SearchButton" Width="75" Content="Szukaj" Click="SzukajTweeta"/>
W VisualStudio, klikając prawym klawiszem myszy na nazwę funkcji, możesz przejść do Navigate to Event Handler, a narzędzie wygeneruje szkielet funkcji i przeniesie cię do edycji kodu funkcji. W tej funkcji będziemy wyszukiwać wpisy pasujące do szukanych fraz. Ponieważ jest to proste wywołanie REST GET, będziemy używać prostego WebClient Silverlight API. Jest to najprostsze możliwe do użycia sieciowe API, pozwalające na odczyt/zapis poprzez polecenia GET/POST, dopóki nie potrzebujesz modyfikować nagłówków. Zanim zaczniemy, zainicjuje kilka zmiennych monitorujących postęp oraz parametry naszych wyszukiwań.
public partial class Search : Page { const string SEARCH_URI = "http://search.twitter.com/search.atom?q={0}&since_id={1}"; private string _lastId = "0"; private bool _gotLatest = false;
Teraz zajmiemy się funkcją. Przypominacie sobie asynchroniczność Silverlight? Teraz zobaczycie ją w akcji. Będziemy używać OpenRead API w WebClient. Ponieważ funkcja będzie asynchroniczna, musimy dodać obsługę zakończonego zdarzenia, w celu otrzymania odpowiedzi. Zaczynamy od:
private void SzukajTweeta(object sender, RoutedEventArgs e) { WebClient proxy = new WebClient(); proxy.OpenReadCompleted += new OpenReadCompletedEventHandler(OnReadCompleted); proxy.OpenReadAsync(new Uri(string.Format(SEARCH_URI, HttpUtility.UrlEncode(SearchTerm.Text), _lastId))); } void OnReadCompleted(object sender, OpenReadCompletedEventArgs e) { throw new NotImplementedException(); }
Na początku tworzymy nową instancje WebClient. Następnie, informujemy o funkcji obsługującej zakończone zdarzenia. Na końcu, wywołujemy funkcje OpenReadAsync. Rezultatem zakończonego zdarzenie (e.Result) będzie strumień (stream). Ponieważ planujemy databinding oraz przetwarzanie otrzymanej odpowiedzi, stworzymy lokalną klase, która będzie reprezentować otrzymane wyniki wyszukiwania. Nazwałem ją TwitterSearchResult.cs i jest to zwykły plik klasy w moim projekcie w katalogu Model. Tworzymy nowy katalog w projekcie:
Dodajemy nową klase w tym katalogu:
Nadajemy nazwę TwitterSearchResult:
Definiujemy klase:
using System; using System.Windows.Media; namespace WyszukiwarkaTwitter.Model { public class TwitterSearchResult { public string Author { get; set; } public string Tweet { get; set; } public DateTime PublishDate { get; set; } public string ID { get; set; } public ImageSource Avatar { get; set; } } }
Mając gotowy model, możemy przetworzyć dane i wykonać databinding.
Inne opcje sieciowe: HttpWebRequest i ClientHttp
Istnieją dwa inne sieciowe API, które możemy użyć przy dostępie do API Twitter-a: HttpWebRequest i ClientHttp. HttpWebRequest używamy tak naprawdę z WebClient, jako, ze jest to dodatkowa warstwa wokół tego API. Oba HttpWebRequest i ClientHttp, korzystają ze stosu sieciowego przeglądarki. Powoduje to pewne ograniczenia, brak możliwości otrzymania kompletnych kodów stanu lub użycia rozszerzonych metod (PUT/DELETE). Silverlight pozwala na stosowanie ClientHttp używającego własny stos sieciowy, który udostępnia więcej metod oraz kodów stanu innych niż 200/404. Więcej informacji w języku angielskim:
Jako przykład użycia ClientHttp, nasze wyszukiwanie wyglądało by tak:
private void SzukajTweeta(object sender, RoutedEventArgs e) { bool httpBinding = WebRequest.RegisterPrefix("http://search.twitter.com", WebRequestCreator.ClientHttp); WebClient proxy = new WebClient(); proxy.OpenReadCompleted += new OpenReadCompletedEventHandler(OnReadCompleted); proxy.OpenReadAsync(new Uri(string.Format(SEARCH_URI, HttpUtility.UrlEncode(SearchTerm.Text)))); }
Pamiętajcie, że w naszym projekcie nie używamy tej metody, ale chciałem ją wam przedstawić w praktycznym użyciu. Odwołanie do RegisterPrefix powoduje, że będziemy używać stosu sieciowego ClientHttp, a nie przeglądarki. W powyższym przykładzie uaktywniliśmy tę opcje tylko dla wywołań do domeny Twitter, ale mogliśmy ustawić ją również na wszystkie wywołania HTTP. Jest to dodatkowa możliwość, do rozważenia w Twoich aplikacjach.
(Binding) Łączymy dane ze sprytnymi obiektami
Ponieważ nasza aplikacja będzie monitorowała podane frazy, chcemy jednorazowo stworzyć połączenie z obiektem przechowującym dane, następnie tylko operować tym obiektem (w naszym przypadku, dodawać dane). W tym celu użyjemy dwóch użytecznych struktur w Silverlight: ObservableCollection<T> i PagedCollectionView. ObservableCollection to typ kolekcji dostarczający powiadomienia w momencie modyfikacji elementów kolekcji (dodanie, usunięcie, aktualizacja). PagedCollectionView użyjemy do automatycznego uporządkowania naszych danych.
Zadeklarujemy je jako zmienne w naszym projekcie:
public partial class Search : Page { const string SEARCH_URI = "http://search.twitter.com/search.atom?q={0}&since_id={1}"; private string _lastId = "0"; private bool _gotLatest = false; ObservableCollection<TwitterSearchResult> searchResults = new ObservableCollection<TwitterSearchResult>(); PagedCollectionView pcv;
Pamiętajmy też o deklaracji potrzebnych bibliotek:
using WyszukiwarkaTwitter.Model; using System.Collections.ObjectModel; using System.Windows.Data;
Bo deklaracji zmiennych, możemy zainicjować PagedCollectioView w kodzie konstruktora, będzie on błyskawicznie dostepny do użycia. Chemy także połączuć nasz interfejs użytkownika (UI) z elementami w XAML. Dobrą praktyką jest nie manipulowanie UI w konstruktorze obsługującym akcje użytkownika (w naszym przypadku Search.xaml). Dlatego dodamy w konstruktorze obsługę zdarzenia pobrano dane (Loaded) i zainicjujemy połączenie w kodzie zdarzenia. Zmieniony konstruktor i dodatkowe zdarzenie:
public Search() { InitializeComponent(); pcv = new PagedCollectionView(searchResults); pcv.SortDescriptions.Add(new System.ComponentModel.SortDescription("PublishDate", System.ComponentModel.ListSortDirection.Ascending)); Loaded += new RoutedEventHandler(Search_Loaded); } void Search_Loaded(object sender, RoutedEventArgs e) { SearchResults.ItemsSource = pcv; }
W obsłudze pobranych danych (Search_Loaded) łączymy UI – atrybut ItemSource elementu DataGrid (SeachResults) z kolekcją PagedCollectionView (pcv), która jest uporządkowana zgodnie z parametrami wywołania pcv.SortDescription (w konstruktorze). Stworzyliśmy połączenie pomiędzy UI a kolekcją danych. Najwyższy czas dostarczyć dane do kolekcji. Pamiętając, że ta kolekcja odzwierciedla wyniki wyszukiwania przechowywane w ObservableCollection<TwitterSearchResult>, to właśnie tu musimy dodawać dane aby móc je zobaczyć w UI.
Wypełnianie danymi kolekcji ObservableCollection
Wracamy do obsługi zdarzenia zakończenia wczytywania rezultatów wyszukiwania (funkcja OnReadCompletion). Będziemy w niej dodawać dane do kolekcji ObservableCollection. Oto kod:
void OnReadCompleted(object sender, OpenReadCompletedEventArgs e) { if (e.Error == null) { _gotLatest = false; XmlReader rdr = XmlReader.Create(e.Result);
SyndicationFeed feed = SyndicationFeed.Load(rdr); foreach (var item in feed.Items) { searchResults.Add(new TwitterSearchResult() { Author = item.Authors[0].Name, ID = GetTweetId(item.Id), Tweet = item.Title.Text, PublishDate = item.PublishDate.DateTime.ToLocalTime(), Avatar = new BitmapImage(item.Links[1].Uri) }); _gotLatest = true; } rdr.Close(); } else { ChildWindow errorWindow = new ErrorWindow(e.Error); errorWindow.Show(); } } private string GetTweetId(string twitterId) { string[] parts = twitterId.Split(":".ToCharArray()); if (!_gotLatest) { _lastId = parts[2].ToString(); } return parts[2].ToString(); }
Nie zapominamy o potrzebnych bibliotekach:
using System.Xml; using System.ServiceModel.Syndication; using System.Windows.Media.Imaging;
Po dodaniu całego kodu i uporaniu się z brakiem referencji do bibliotek (patrz poniżej), przyjrzyjmy się mu bliżej. e.Results (e to parametr funkcji OnReadCompleted) zawiera strumień danych otrzymany z udanego przeszukiwania Twitter-a. Jeśli wystąpiły jakieś błędy, użyjemy szablonu ErrorWindow będącego częścią wybranego szkieletu projektu (Silverlight Navigation Aplication). Zmienna _gotLatest pomaga w wyszukiwaniu tylko nowych wpisów Twitter-a (stworzonych po uprzednim przeszukiwaniu). Po otrzymaniu strumienia danych, ładujemy do XMLReader w celu łatwiejszej transformacji do klasy SyndicationFeed. Jest to klasa posiadająca wbudowane funkcje do przetwarzania informacji ze standardowych strumieni danych jak RSS lub Atom. Następnie przeglądamy wszystkie wpisy zlokalizowane teraz w obiekcie klasy SyndicationFeed i dodajemy je do obiektu naszej własnej klasy ObservableCllection<TwitterSearchResult>. Pewnie zauważycie, że wykonujemy pewną konwersje obrazka awatara z formatu Image URI na format ImageSource, w celu ułatwienia późniejszego połączenia (binding). Dodatkowo przeglądamy identyfikatory wpisów w celu znalezienia najświeższego wpisu i użycia jego identyfikatora jako _lastID w następnym wyszukiwaniu.
Notka: System.ServiceModel.Syndication zawiera wiele zależności do innych bibliotek. To nie jest mała biblioteka, ale wygodna w użyciu. Pamiętaj o tym gdy chcesz jej użyć w projekcie, rozważ za i przeciw. Używamy jej w naszym projekcie w celu zapoznania was z jej możliwościami i wygodą użycia. Alternatywną metodą (głównie do wczytywania złożonych (syndicated) danych) byłoby użycie LINQ na XAML i praca z uzyskanym po wczytaniu danych Xdocument-em. Więcej informacji w języku angielskim o pracy ze złożonymi (syndicated) danymi:
Osobiście (Irek) miałem problem z dodaniem biblioteki ServiceModel.Syndication. Wpisanie w kod powodowało błąd i sygnalizowało brak istnienia takiej biblioteki. Dopiero prawy klik na References w widoku projektu i ręczne dodanie tej biblioteki pomogło:
Informowanie użytkownika o postępach monitorowania
W końcowym kroku chcemy powiadomić użytkownika naszej aplikacji, że właśnie trwa wyszukiwanie. W oryginalnym samouczku autor opowiada o kontrolce ActivityControl, którą należało pobrać i dodać do projektu. Wspomina także o nowej kontrolce BusyIndicator dostępnej w Silverlight Toolkit. Zajeło mi niezłą chwilę zanim udało mi się to wszystko poskładać w całość, a na koniec okazało się takie proste. Oto co robimy, najpierw w pliku Search.xaml, dodajemy nową kontrolkę BusyIndicator, która jest nadrzędna do wszystkich pozostałych elementów strony. Pamiętajmy o referencji do biblioteki (xmlns:toolkit=):
<navigation:Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit" x:Class="WyszukiwarkaTwitter.Views.Search" mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480" Title="Search Page"> <toolkit:BusyIndicator x:Name="BusyIndicator"> <Grid x:Name="LayoutRoot"> <Grid.RowDefinitions> <RowDefinition Height="32"/> <RowDefinition/> </Grid.RowDefinitions> <StackPanel HorizontalAlignment="Left" Margin="0,-32,0,0" VerticalAlignment="Top" Grid.Row="1" Orientation="Horizontal"> <TextBox x:Name="SearchTerm" FontSize="14.667" Margin="0,0,10,0" Width="275" TextWrapping="Wrap"/> <Button x:Name="SearchButton" Width="75" Content="Szukaj" Click="SzukajTweeta"/> </StackPanel> <sdk:DataGrid x:Name="SearchResults" Grid.Row="1"/> </Grid> </toolkit:BusyIndicator> </navigation:Page>
Teraz przechodzimy do kodu c# i pliku Search.xaml.cs. Uaktywniamy pasek postępu przed rozpoczęciem wyszukiwania w funkcji SzukajTweta:
private void SzukajTweeta(object sender, RoutedEventArgs e) { BusyIndicator.IsBusy = true; WebClient proxy = new WebClient(); proxy.OpenReadCompleted += new OpenReadCompletedEventHandler(OnReadCompleted); proxy.OpenReadAsync(new Uri(string.Format(SEARCH_URI, HttpUtility.UrlEncode(SearchTerm.Text), _lastId))); }
Wyłączamy pasek po zakończeniu wczytywania danych czyli na końcu funkcji OnReadCompleted:
void OnReadCompleted(object sender, OpenReadCompletedEventArgs e) { if (e.Error == null) { _gotLatest = false; XmlReader rdr = XmlReader.Create(e.Result);
SyndicationFeed feed = SyndicationFeed.Load(rdr);
foreach (var item in feed.Items) { searchResults.Add(new TwitterSearchResult() { Author = item.Authors[0].Name, ID = GetTweetId(item.Id), Tweet = item.Title.Text, PublishDate = item.PublishDate.DateTime.ToLocalTime(), Avatar = new BitmapImage(item.Links[1].Uri) }); _gotLatest = true; } rdr.Close(); } else { ChildWindow errorWindow = new ErrorWindow(e.Error); errorWindow.Show(); } BusyIndicator.IsBusy = false; }
Pasek postępu w działaniu:
Po odczekaniu chwili, możemy zobaczyć wyniki:
Dodajemy monitorowanie wyszukiwania
Jako, że nasza aplikacja ma regularnie sprawdzać nowe wpisy dotyczące podanej frazy, chcemy zautomatyzować odświeżanie wyników wyszukiwania. W Silverlight mamy kilka możliwości implementacji automatycznej aktywności. W naszej aplikacji użyjemy DispatcherTimer. Jest to nic innego jak stoper, który uruchamia zdarzenia w określonych odstępach. Dodamy kolejną zmienną (_timer):
public partial class Search : Page { const string SEARCH_URI = "http://search.twitter.com/search.atom?q={0}&since_id={1}"; private string _lastId = "0"; private bool _gotLatest = false; ObservableCollection<TwitterSearchResult> searchResults = new ObservableCollection<TwitterSearchResult>(); PagedCollectionView pcv; DispatcherTimer _timer;
Następnie w konstruktorze, inicjujemy nasz stoper i dodajemy nowe zdarzenie odpalane w cyklu zdefiniowanym odstępem (interval):
public Search() { InitializeComponent(); double interval = 30.0; _timer = new DispatcherTimer(); #if DEBUG interval = 10.0; #endif _timer.Interval = TimeSpan.FromSeconds(interval); _timer.Tick += new EventHandler(OnTimerTick);
Zauważmy, że zmienna interval przyjmuje dwie wartości, 30 sekund gdy aplikacja pracuje normalnie i 10 sekund gdy debugujemy aplikacje (F5). Teraz musimy zmodyfikować (refactor) kod aby funkcja SzukajTweeta była odpalana w zadanym odstępie. Użyjemy możliwości Visual Studio i przeniesiemy (refactor) kod funkcji SzukajTweeta do nowej funkcji SzukajTweetaEx, która to będziemy odpalać w obsłudze zdarzenia OnTimerTick. Zaznaczamy wnętrze funkcji SzukajTweeta, prawy przycisk myszy ?> refactor -> extract method:
Dodajemy wywołanie nowej funkcji w zdarzeniu OnTimerTick:
void OnTimerTick(object sender, EventArgs e) { SzukajTweetaEx(); }
Musimy także zainicjować stoper i pierwsze wyszukiwanie:
void Search_Loaded(object sender, RoutedEventArgs e) { SearchResults.ItemsSource = pcv; //połacz z kontrolką DataGrid _timer.Start(); // włącz stoper SzukajTweetaEx(); // wykonaj pierwsze przeszukanie }
Dodajemy obsługę zbyt długo trwających wyszukiwań:
private void SzukajTweetaEx() { if (!string.IsNullOrEmpty(SearchTerm.Text)) { _timer.Stop(); //zatrzymaj stoper gdy wyszukujemy dłużej niż zdefiniowany interwał BusyIndicator.IsBusy = true; //włącz pasek postępu //przeszukujemy Twitter-a i obsługujemy wyniki wyszukiwania WebClient proxy = new WebClient(); proxy.OpenReadCompleted += new OpenReadCompletedEventHandler(OnReadCompleted); proxy.OpenReadAsync(new Uri(string.Format(SEARCH_URI, HttpUtility.UrlEncode(SearchTerm.Text), _lastId))); } }
Na końcu zerujemy stoper po wczytaniu wszystkich wyników (ostatnia linia funkcji OnReadCompleted). Oto kompletny kod pliku Search.xaml.cs:
using System; using System.Net; using System.Net.Browser; using System.Windows; using System.Windows.Data; using System.Windows.Browser; using System.Windows.Controls; using System.Windows.Navigation; using System.Windows.Threading; using System.Windows.Media.Imaging; using System.Collections.ObjectModel; using System.Xml; using System.ServiceModel.Syndication; using WyszukiwarkaTwitter.Model; namespace WyszukiwarkaTwitter.Views { public partial class Search : Page { const string SEARCH_URI = "http://search.twitter.com/search.atom?q={0}&since_id={1}"; private string _lastId = "0"; private bool _gotLatest = false; ObservableCollection<TwitterSearchResult> searchResults = new ObservableCollection<TwitterSearchResult>(); PagedCollectionView pcv; DispatcherTimer _timer; public Search() { InitializeComponent(); double interval = 30.0; _timer = new DispatcherTimer(); #if DEBUG interval = 10.0; #endif _timer.Interval = TimeSpan.FromSeconds(interval); _timer.Tick += new EventHandler(OnTimerTick); pcv = new PagedCollectionView(searchResults); pcv.SortDescriptions.Add(new System.ComponentModel.SortDescription("PublishDate", System.ComponentModel.ListSortDirection.Ascending)); Loaded += new RoutedEventHandler(Search_Loaded); } void OnTimerTick(object sender, EventArgs e) { SzukajTweetaEx(); } void Search_Loaded(object sender, RoutedEventArgs e) { SearchResults.ItemsSource = pcv; //połacz z kontrolką DataGrid _timer.Start(); // włącz stoper SzukajTweetaEx(); // wykonaj pierwsze przeszukanie } // Executes when the user navigates to this page. protected override void OnNavigatedTo(NavigationEventArgs e) { } private void SzukajTweeta(object sender, RoutedEventArgs e) { SzukajTweetaEx(); } private void SzukajTweetaEx() { if (!string.IsNullOrEmpty(SearchTerm.Text)) { _timer.Stop(); //zatrzymaj stoper gdy wyszukujemy dłużej niż zdefiniowany interwał BusyIndicator.IsBusy = true; //włącz pasek postępu //przeszukujemy Twitter-a i obsługujemy wyniki wyszukiwania WebClient proxy = new WebClient(); proxy.OpenReadCompleted += new OpenReadCompletedEventHandler(OnReadCompleted); proxy.OpenReadAsync(new Uri(string.Format(SEARCH_URI, HttpUtility.UrlEncode(SearchTerm.Text), _lastId))); } } void OnReadCompleted(object sender, OpenReadCompletedEventArgs e) { if (e.Error == null) { _gotLatest = false; //wyzeruj flage ostatniego wyniku XmlReader rdr = XmlReader.Create(e.Result); //załaduj wyniki do czytnika XML SyndicationFeed feed = SyndicationFeed.Load(rdr); //wypełnij złożone dane (Atom) //załaduj każdy wynik do naszej kolekcji foreach (var item in feed.Items) { searchResults.Add(new TwitterSearchResult() { Author = item.Authors[0].Name, ID = GetTweetId(item.Id), Tweet = item.Title.Text, PublishDate = item.PublishDate.DateTime.ToLocalTime(), Avatar = new BitmapImage(item.Links[1].Uri) }); _gotLatest = true; //ustaw flage informującą, że mamy Id ostatniego wyniku } rdr.Close(); //zamknij czytnik } else { //zainicjuj komunikat o błędzie ChildWindow errorWindow = new ErrorWindow(e.Error); errorWindow.Show(); } BusyIndicator.IsBusy = false; //aktywuj UI _timer.Start(); //wyzeruj stoper } private string GetTweetId(string twitterId) { string[] parts = twitterId.Split(":".ToCharArray()); if (!_gotLatest) { _lastId = parts[2].ToString(); } return parts[2].ToString(); } } }
Nasza strona wyszukiwania, wyzeruje czas i zacznie pierwsze wyszukiwanie. Po podanym odstępie znowu wykona wyszukiwanie, mając w pamięci identyfikator ostatniego wpisu, nie będzie wczytywała ponownie starych wpisów. Nowsze wpisy będą dodawane do naszej kolekcji (ObservableCollection), a ta z uwagi na połączenie (binding) z kontrolką DataGrid, automatycznie wyświetli je na stronie.
Dodaliśmy także sprawdzanie czy podana fraza nie jest pusta:
private void SzukajTweetaEx() { if (!string.IsNullOrEmpty(SearchTerm.Text)) //sprawdź czy wprowadzono fraze {
Podsumowanie
W części 3 wykonaliśmy kawał roboty. Połączyliśmy się z usługa internetową, powiązaliśmy wyniki zapytań z kontrolką DataGrid oraz dodaliśmy stoper do automatyzacji wyszukiwania. W sumie można by zakończyć, ale my chcemy więcej – kontrolka DataGrid nie jest najlepszą formą UI. Czas na część 4 gdzie poznamy szablony danych i wprowadzenie do składni powiązań (binding) w XAML.