Reactive Extensions ile Twitter Üzerinde Canlı Arama

Örnek uygulamayı buradan indirebilirsiniz: Rx-TwitterSearch.zip

Örnek uygulama olarak Twitter Search API kullanarak http://search.twitter.com/search.json url'sine istekte bulunup sonuçları listeleyeceğiz ve paging yapısı implemente edeceğiz. Bu örneğimizi WPF ile geliştireceğimiz için Reactive Extensions WPF Helpers paketine ihityacımız olacak. Bir WPF projesi oluşturduktan sonra NuGet ile bu paketleri projeye ekleyebiliriz.

PM> Install-Package Rx-WPF

 

Temel UI bileşenlerini panele yerleştirerek uygulamamıza başlayalım. Arama yapmak ve listelemek için bir TextBox ve bir ListBox ekleyelim.

 

<Window x:Class="TwitterSearch.MainWindow"
      
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      
Title="MainWindow" Height="500" Width="400">

       <DockPanel Height="Auto" Width="Auto">
              <TextBox x:Name="txtKeyword" Height="30" Text="Arama..." Width="Auto" DockPanel.Dock="Top"/>               <ListBox x:Name="listResult" Height="440" Width="Auto" DockPanel.Dock="Bottom"/>               </DockPanel>

</Window>

 

Kod tarafına geri dönelim ve TextBox elementinin TextChanged event'iyle bir Observable yaratalım;

var search = Observable
             
.FromEventPattern<TextChangedEventArgs>(txtKeyword, "TextChanged")
              .Select(_event => (_event.Sender
as TextBox).Text);

FromEventPattern metodunun çağırımıyla birlikte Reactive, parametre olarak verilen txtKeyword instance'inin TextChanged eventini reflection ile buluyor ve bu eventi bir observable nesnesine dönüştürüyor. Select metoduyla, çağırılmış eventin sahibinin (TextBox) Text propertysini alıyoruz.

search.Subscribe(keyword => Debug.WriteLine(string.Format("TextChangedEvent Value= {0}", keyword)));

search nesnesi üzerinden TextChanged eventininin göndermiş olduğu argumanı alıp konsola bastırdığımızda aşağıdaki gibi bir output elde edeceğiz.

 

Şu ana kadar yaptığımız işlemi bilinen şekilde aşağıdaki gibi gerçekleştirebiliriz

txtKeyword.TextChanged+= txtKeyword_TextChanged;

private
void txtKeyword_TextChanged(object sender, TextChangedEventArgs e)
{
      
Debug.WriteLine(string.Format("txtKeyword_TextChanged= {0}", (sender as TextBox).Text));
}

 

Şimdi de konsola bastırmak yerine, listbox içerisine ekleyelim.

ObservableCollection<string> listSource = new ObservableCollection<string>();
listResult.ItemsSource = listSource;
int i = 1;

var search = Observable
             
.FromEventPattern<TextChangedEventArgs>(txtKeyword, "TextChanged")
              .Select(_event => (_event.Sender
as TextBox).Text)
              .Subscribe(keyword => listSource.Add(
string.Format("{0}. {1}", i++, keyword)));

 

Her yazdığımız karakter txtKeyword.TextChanged eventinin tetiklenmesine neden oluyor, ve observable tarafından izlenen bu değişiklik ListBox elemanlarına ekleniyor. Bu şekilde yapılacak bir arama işlemi sunucu tarafını isteklere boğacak, ardından istemcinin listeyi güncellemesi zamanında bir yoğunluk oluşturacak ve main thread in uzun süreli beklemesine neden olacaktır.

Bu tür UI güncelleme işlemleri için Reactive Extensions WPF Helper paketi içerisinde thead yönetimini sağlayan yapılar bulunmaktadır. System.Reactive.Windows.Threading librarysi içerisinde yer alan ObserveOnDispatcher metodu, çağırdığımız observable handler'ini main thread dışarısında çalıştıracaktır. Ek olarak standart Rx eklentisi içerisindeki Throttle metoduyla observable içerisinde yalnızca belirli zaman içerisinde yapılan değişikleri yakalayabiliriz.

ObserveOnDispatcher ve Throttle metodlarını observable nesnesine dahil edelim;

var search = Observable
             
.FromEventPattern<TextChangedEventArgs>(txtKeyword, "TextChanged")
              .Select(_event => (_event.Sender
as TextBox).Text)
              .Throttle(TimeSpan.FromMilliseconds(600))
              .Where(keyword => keyword.Length > 0)
              .ObserveOnDispatcher()

              .Subscribe(keyword => listSource.Add(
string.Format("{0}. {1}", i++, keyword)));

Throttle metodu, belirttiğimiz 600 milisaniyelik zaman içerisinde yapılan değişiklikleri event olarak yakalayacaktır. Where içerisinde de tetiklenen event'in gözlemleneceği kriteri belirlemiş olduk. Bu durumda uzunluğu 0 karakterden fazla olan ve 600 milisaniye içerisindeki tüm değişiklikleri yansıtmış oluyoruz.

 

 


Buraya kadar, Twitter içinde arama yapmak üzere arama sözcüğünü elde edebiliyoruz. Şimdi de arama için sunucuya istek gönderimi ve gelen isteği parse etme işlemine geçelim.

Öncelikli olarak search.twitter.com/search.json url'ine istekte bulunalım ve gelen yanıtı inceleyelim.

 

 

 

Aldığımız json nesnesini C# nesnesine dönüştürmek için gereksinimimiz doğrultusunda aşağıdaki gibi bir sınıf oluşturabiliriz.

        public class TwitterSearchResult
        {
           
public List<Tweet> results { get; set; }

           
public class Tweet
            {
               
public string created_at { get; set; }
               
public string from_user { get; set; }
               
public string profile_image_url { get; set; }
               
public string text { get; set; }
                    
                public string ToString()
                {
                   
return string.Format("@{0} {1}", this.from_user, this.text);
                }
            }
        }

Twitter Search API url'ine basit bir istekte bulunup, gelen json yanıtını yukarıda oluşturduğumuz nesneye deserialize etmek için System.Net.WebClient ve Newtonsoft.Json.JsonConvert sınıflarını kullanabiliriz.

public TwitterSearchResult Download(string keyword)
{
       string url = string.Format("http://search.twitter.com/search.json?q={0}", keyword);
      
using (WebClient w = new WebClient())
       {
             
string json = w.DownloadString(url);
             
return JsonConvert.DeserializeObject<TwitterSearchResult>(json);
       }
}

 

Şimdiki aşamada ise observable nesnesinin yakaladığı TextChanged eventinin parametresini Download metoduna gönderelim ve TwitterSearchResult.results içerisindeki sonuçları ListBox'ta gösterelim.

var search = Observable.FromEventPattern<TextChangedEventArgs>(txtKeyword, "TextChanged")
              .Select(_event => (_event.Sender
as TextBox).Text)
              .Throttle(
TimeSpan.FromMilliseconds(600))
              .Where(keyword => keyword.Length > 0)
              .Select(keyword => {
return Download(keyword); })
              .ObserveOnDispatcher()
              .Subscribe(result =>
              {
                     listSource.Clear();
                     result.results.ForEach(tweet => listSource.Add(tweet.ToString()));
              });


 

Twitter Search API kullanımını incelediğimizde bize sonuçlar arasında hangi sayfayi gösterdiğimizi ve bir sonraki sayfa sayısını parametre olarak kabul ettiğini görebiliriz. O halde şu anki arama urlini aşağıdaki gibi düzenleyip sayfa parametresini de eklersek bize istediğimiz sayfadaki sonuçları getirebilecek, ve bunları listeleyecek bir sorguya sahip oluruz.

"http://search.twitter.com/search.json?q={0}&page={1}"

WebClient ile istekte bulunduğumuz Download metodunu, keyword ve page parametrelerini kabul edecek şekilde güncelleyelim.

public TwitterSearchResult Download(string keyword, int pageNo)
{
       string url = string.Format("http://search.twitter.com/search.json?q={0}&page={1}",
                                                                           keyword, pageNo);
       using (WebClient w = new WebClient())
       {
             
string json = w.DownloadString(url);

              return JsonConvert.DeserializeObject<TwitterSearchResult>(json);

}

}

Listelediğimiz sonuçları önceki sayfalara doğru görmek için kaydırma çubuğunu aşağı kaydıracağız ve bir sonraki sayfa için Twitter Search API url’ine istekte bulunacağız. Bunun için de ListBox’ın ScrollBar elementine, ve bu elementin de Scroll eventine gereksinimimiz olacak.

 

 

 

 

WPF içerisinde ListBox elementinin ScrollBar elementine direkt olarak erişemiyoruz, ancak VisualTreeHelper sınıfını aşağıdaki gibi kullanarak bu işlemi gerçekleştirebiliriz.

ScrollBar scrollBar =FindVisualChild<ScrollBar>(listResult);

public static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject

        {

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)

            {

                DependencyObject child = VisualTreeHelper.GetChild(obj, i);

                if (child != null && child is T)

                {

                    return (T)child;

                }

                else

                {

                    T childOfChild = FindVisualChild<T>(child);

                    if (childOfChild != null)

                    {

                        return childOfChild;

                    }

                }

            }

            return null;

        }

 

Artık scrollbar elementine erişebiliyoruz. O halde ScrollBar.Scroll eventiyle bir observable nesne yaratalim;

var paging = Observable.FromEventPattern<ScrollEventArgs>(scrollBar, "Scroll")

       .Select(evt => ((ScrollBar)evt.Sender).Value)

       .Where(val => val == scrollBar.Maximum);

Where metoduyla birlikte sağladığımız kriterde, scrollBar’ın şu anki pozisyonu maximum değerine ulaştığında scroll eventi observable tarafından yakalanacaktır.

 

int _pageNo = 1;

string _keyword = string.Empty;

var
paging = Observable.FromEventPattern<ScrollEventArgs>(scrollBar, "Scroll")

.Select(evt => ((ScrollBar)evt.Sender).Value)

       .Where(val => val == scrollBar.Maximum)

       .Throttle(TimeSpan.FromMilliseconds(400))

       .Select(val => { return Download(_keyword, _pageNo + 1); })

       .ObserveOnDispatcher()

       .Subscribe(result =>

       {

              result.results.ForEach(tweet => listSource.Add(tweet.ToString()));

              _pageNo++;

});

 

 

 

 

Son durumda aşağıdaki gibi observable nesnelerimiz olacaktir;

int _pageNo = 1;

string _keyword = string.Empty;

 

var search = Observable.FromEventPattern<TextChangedEventArgs>(txtKeyword, "TextChanged")

                .Select(_event => (_event.Sender as TextBox).Text)

                .Throttle(TimeSpan.FromMilliseconds(600))

                .Where(keyword => keyword.Length > 0)

                .Select(keyword =>

                {

                    _keyword = keyword;

                    return Download(keyword, _pageNo);

                })

                .ObserveOnDispatcher()

                .Subscribe(result =>

                {

                    listSource.Clear();

                    result.results.ForEach(tweet => listSource.Add(tweet.ToString()));

                });

 

 


ScrollBar
scrollBar = FindVisualChild<ScrollBar>(listResult);


var
paging = Observable.FromEventPattern<ScrollEventArgs>(scrollBar, "Scroll")

                .Select(evt => ((ScrollBar)evt.Sender).Value)

                .Where(val => val == scrollBar.Maximum)

                .Throttle(TimeSpan.FromMilliseconds(400))

                .Select(val => { return Download(_keyword, _pageNo + 1); })

                .ObserveOnDispatcher()

                .Subscribe(result =>

                {

                    result.results.ForEach(tweet => listSource.Add(tweet.ToString()));

                    _pageNo++;

                });


Burada belirtmem gereken bir detay şu; WPF yapısında UI tam olarak yüklenmeden buradaki elementlere erişimin yapılamaması. Bu nedenle observable nesnelerimizi yaratırken VisualTree’nin oluşmasını beklemeliyiz. Bunun için en kısa çözüm ekranımızdaki panelin load eventinde observable nesnelerini oluşturacağımız metodu çağırmak.

 private void DockPanel_Loaded_1(object sender, RoutedEventArgs e)

 {

Bind();

 }

 

        private void Bind()

        {

 

            txtKeyword.Focus();

 

            ObservableCollection<string> listSource = new ObservableCollection<string>();

            listResult.ItemsSource = listSource;

 

            int _pageNo = 1;

            string _keyword = string.Empty;

 

            var search = Observable.FromEventPattern<TextChangedEventArgs>(txtKeyword, "TextChanged")

                .Select(_event => (_event.Sender as TextBox).Text)

                .Throttle(TimeSpan.FromMilliseconds(600))

                .Where(keyword => keyword.Length > 0)

                .Select(keyword =>

                {

                    _keyword = keyword;

                    return Download(keyword, _pageNo);

                })

                .ObserveOnDispatcher()

                .Subscribe(result =>

                {

                    listSource.Clear();

                    result.results.ForEach(tweet => listSource.Add(tweet.ToString()));

                });

 

 

            ScrollBar scrollBar = FindVisualChild<ScrollBar>(listResult);

            var paging = Observable.FromEventPattern<ScrollEventArgs>(scrollBar, "Scroll")

                .Select(evt => ((ScrollBar)evt.Sender).Value)

                .Where(val => val == scrollBar.Maximum)

                .Throttle(TimeSpan.FromMilliseconds(400))

                .Select(val => { return Download(_keyword, _pageNo + 1); })

                .ObserveOnDispatcher()

                .Subscribe(result =>

                {

                    result.results.ForEach(tweet => listSource.Add(tweet.ToString()));

                    _pageNo++;

                });

        }

 


Artık uygulamayı calıştırdığımızda, arama yapıp scrollbarı kaydırınca aşağıdaki gibi sayfalama elde edebileceğiz.

 

Son olarak görseli de biraz düzenleyelim. ListBox içerisine tanımlayacağımız ItemTemplate ile, arama sonuçlarında gösterilecek her tweet için layout template belirleyebiliriz.

<ListBox x:Name="listResult" ItemsSource="{Binding}" >

<ListBox.ItemTemplate><DataTemplate>

<StackPanel Orientation="Horizontal" Height="96" Width="410">


<
Image Source="{Binding profile_image_url}" HorizontalAlignment="Left" Height="76" VerticalAlignment="Top" Width="76" Margin="10,10,10,5"/>


<StackPanel Orientation="Vertical" Width="Auto" Margin="5,10,10,10">


             
<StackPanel Orientation="Horizontal" Width="Auto" >


<
Label FontSize="14" Content="{Binding from_user}" FontWeight="Bold" Width="200" HorizontalAlignment="Left" VerticalAlignment="Center" Height="28" />


<
Label FontSize="12" Content="{Binding created_at}" Width="100" HorizontalAlignment="Right" VerticalAlignment="Center" Height="28" />

              </StackPanel>


<TextBlock TextWrapping="Wrap" FontSize="12" Text="{Binding text}"  HorizontalAlignment="Left" VerticalAlignment="Stretch" Width="300"/>

    </StackPanel>

</StackPanel>

</DataTemplate></ListBox.ItemTemplate>

</ListBox>


Bu template tanımıyla birlikte uygulamamızın son görünümü aşağıdaki gibi olacaktır.

 

 

reactiveextensionscsharpwpfxaml
deniz ozgen tarafından yazıldı.

Yorumlar

Barış G&#252;rb&#252;z
Barış Gürbüz
11 Eyl, 2013 06:41

Tebrik ederim başarılı bir yazı olmuş. Rx ile ilgili daha geniş bilgi vermenizi rica edebilir miyim?

17 Şub, 2017 01:00

I am interested to build up my career as a pilot. Suggest me some health tips to keep my body,eyesight,etc. fit?

Yorumlarınız

Yorum

Önizleme