2010年1月27日水曜日

第10回 ハイパーリンクを作る

今回は名前をクリックしたら、そのユーザのタイムラインをブラウザで表示するようにしてみたいと思います。

まずTimelineItemViewModelに、そのユーザのタイムラインのURLを返すプロパティを追加します。


  public string UserUrl {
   get { return string.Format(@"http://www.twitter.com/{0}",Name); }
  }



次にイベントが発生する仕組みを作ります。
ビューの名前の部分にClickイベントを作るのが一番早いのですが、
WPFにはコマンドというものが用意されています。
注意が必要なのは、Commandクラス自体はシンボルのようなもので、機能(動作)は持ちません。
例えば、Windouwsは一般的にCtrl+Cで『コピー』コマンドを実行できますが、
動作は、テキスト選択中の場合とファイル選択中の場合では違いますよね?
コマンドとその動作を結びつけるのがCommandBindingです。
ビューやコントロールがCommandBindingを持っていて、
このコマンドが要求されたときはこのメソッドを呼ぶ、というような関連づけがされています。
まずコマンドを作ります。


namespace WTwitter.Command {
 public class Commands {
  static public RoutedUICommand OpenByBrowser
   = new RoutedUICommand("タイムラインをブラウザで開く", "OpenByBrowser", typeof(Commands));
 }
}




コマンドはアプリケーション全体で共用して利用されることが多いので、
staticで宣言されることが多いようです。

RoutedUICommandを使っていますが、Routedがついていると、
そのコントロールでコマンドが処理されなかったとき親のコントロールに処理を依頼します。

※書いている時点で思いましたが、コマンドのクラスもフィールドを直接見せないで
プロパティにした方がいいと思います。そのうち修正します。

最後にTimelineViewを変更します。
まずUserControlのCommandBindingに先ほど作ったコマンドを登録します。
これで、コマンドが実行されたらCommandBinding_Executed()が呼び出されます。


 <UserControl.CommandBindings>
  <CommandBinding Command="{x:Static command:Commands.OpenByBrowser}" Executed="CommandBinding_Executed"/>
 </UserControl.CommandBindings>



次に、名前をHyperLinkにしてコマンドを発行するようにします。
Textboxの中にHyperlink、さらに中にTextboxを配置するまどろっこしい形になっているのは
HyperlinkとBindingを使いたい場合はしょうがないみたいです。


      <TextBlock DockPanel.Dock="Top">
       <Hyperlink Command="{x:Static command:Commands.OpenByBrowser}" CommandParameter="{Binding Path=UserUrl}" >
        <TextBlock Text="{Binding Path=Name}"/>
       </Hyperlink>
      </TextBlock>



最後に、ブラウザの呼び出し部分


  private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) {
   Process.Start(e.Parameter as string);
  }



今回のコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/39432

第9回 時間順に並べる

前回までのコードを実行した人は気づいたかもしれませんが、
起動時には最新のつぶやきが一番上に来るのに、
次のタイマ時刻のつぶやきからは下に追加されていきます。

というわけで今回はソート機能を追加します。

まず、時間を取得します。
私が知らないだけかもしれませんが、
JSONから直接DateTime型で取得できないみたいなので、
いったんstringで受け取ってから変換します。



  [DataMember(Name = "created_at")]
  private string CreatedAtString { get; set; }

  private DateTime? _createdAt = null;
  public DateTime CreatedAt {
   get {
    if (_createdAt == null) {
     _createdAt = DateTime.ParseExact(CreatedAtString, "ddd MMM dd HH:mm:ss zzzz yyyy",
      CultureInfo.GetCultureInfoByIetfLanguageTag("en-us"));
    }
    return _createdAt.Value;
   }
  }



CreatedAtの方にはDataMember属性がついていないことに注意してください。
このためJSON形式としては読み込まれず(=nullのまま)、
初めてgetが呼び出されたときにParseExactで文字列からDateTime型に変換します。

このCreatedAtはViewModelにも追加しています。

次にViewを変更します。
複雑になりそうなので、タイムライン部分をTimelineViewクラスに切り出しました。

よってMainWindowは以下のように簡単になります。



<Window x:Class="WTwitter.View.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:vmt="clr-namespace:WTwitter.ViewModel.Twitter"
 xmlns:vw="clr-namespace:WTwitter.View"
 xmlns:CompModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    Title="WTwitter" Height="500" Width="400">
 <Window.Resources>
  <DataTemplate DataType="{x:Type vmt:TimelineViewModel}">
   <vw:TimelineView/>
  </DataTemplate>
 </Window.Resources>

 <TabControl ItemsSource="{Binding Path=Timelines}">
  <TabControl.ItemTemplate>
   <DataTemplate>
    <TextBlock Text="{Binding Path=DisplayName}"/>
   </DataTemplate>
  </TabControl.ItemTemplate>
 </TabControl>
</Window>





見ての通り、TabControlのContentTemplateがなくなりました。
そのかわりWindow.ResourceにDataTemplateを追加しています。
これは、TimelineViewModel型の表示を要求されたら、
TimelineViewを使うということです。
TabControlには依然としてItemsSourceとしてTimelinesを指定しているので、
Timelineを表示しようとして、そのときにDataTemplateがあるのでそっちを見に行くという仕組みです。

ではTimelineViewを見てみます。


<UserControl x:Class="WTwitter.View.TimelineView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:compModel="clr-namespace:System.ComponentModel;assembly=WindowsBase">
 <UserControl.Resources>
  <CollectionViewSource  x:Key="AllItemsViewSource" Source="{Binding Path=AllItems}">
   <CollectionViewSource.SortDescriptions>
    <compModel:SortDescription PropertyName="CreatedAt" Direction="Descending"/>
   </CollectionViewSource.SortDescriptions>
  </CollectionViewSource>
 </UserControl.Resources>
 
 <ListBox ItemsSource="{Binding Source={StaticResource AllItemsViewSource}}"
以下略




ResourceとしてCollectionViewSourceを追加していて、
(=CollectionViewSourceのインスタンスを生成して、UserControlのリソースとして追加)
ListBoxのItemsSourceにそれを指定しています。
ListBoxに直接Sort方法をしていする方法はないみたいで、
CollectionViewSourceを通じて指定しています。
CollectionViewSourceは私もよく説明できないのですが、
Collectionを表示するようなViewを操作する一般的な方法を集めた
あらかじめ用意されたViewModelのようなもので、
どのようなViewにするかに依存しないでリストを操作できます。

今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/39354

2010年1月25日月曜日

第8回 認証が必要なタイムラインにアクセスする

これまでユーザーIDとパスワードが必要なタイムラインへのアクセスを避けていたので
今回はそこを作ります。

まずユーザーIDとパスワードを保持するクラスを作ります。



namespace WTwitter.Model.UserData {
 /// <summary>
 /// ログイン情報を保持するクラス
 /// </summary>
 public class UserInfo {
  public string UserName { get; set; }
  public string Password { get; set; }
 }
}



本当はパスワードにはセキュリティのためSecureStringクラスとかを使った方がいいのですが、
今回は簡便化のためふつうのstringにしています。

次にIDとパスワードを入力するためのダイアログ(ウィンドウ)を作ります。



<Window x:Class="WTwitter.View.OptionDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="オプション設定" Width="190" Height="120" Loaded="Window_Loaded">
 <Window.BindingGroup>
  <BindingGroup/>
 </Window.BindingGroup>
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="Auto"/>
   <ColumnDefinition Width="Auto"/>
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
   <RowDefinition Height="Auto"/>
   <RowDefinition Height="Auto"/>
   <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>
  <TextBlock Text="UserName" Grid.Row="0" Grid.Column="0"/>
  <TextBox Grid.Row="0" Text="{Binding Path=UserName}"
     Grid.Column="1" Width="100"/>
  
  <TextBlock Text="Password" Grid.Row="1" Grid.Column="0"/>
  <TextBox Text="{Binding Path=Password}"
   Grid.Row="1" Grid.Column="1" Width="100"/>
  
  <Button Grid.Row="2" Grid.Column="1" IsDefault="True" Click="OkButton_Click">OK(_O)</Button>
    </Grid>
</Window>






 public partial class OptionDialog : Window {
  public OptionDialog(UserInfo target) {
   this.DataContext = target;
   InitializeComponent();
  }

  private void Window_Loaded(object sender, RoutedEventArgs e) {
   BindingGroup.BeginEdit();
  }

  private void OkButton_Click(object sender, RoutedEventArgs e) {
   BindingGroup.CommitEdit();
   this.Close();
  }
 }



ちょっと前回まで説明が長かったので、今回からさくっと説明します。
適宜コメントか検索で。

XAMLの方。
ダイアログは2列×3行のGrid(表みたいな入れ物)を作ってテキストボックスなどを配置しています。
先ほど作ったUserInfoクラスをDataContextに入れるので、
それぞれのテキストボックスはUserInfoクラスのプロパティへのパスを指定しています。
WindowにBindingGroupを作っています。
これがあるとBindingGroup.CommitEdit()を呼び出したときに
テキストボックスの中身がUserInfoのプロパティに反映されます。

コードビハインド(.cs)の方はその呼び出しだけです。

あとはApp.xaml.csを以下のように変更しました。
これまでのWindow1クラスは名前が適当すぎたので新しくMainWindowクラスを作りました。
そしてダイアログを通じてUserInfoを取得してもらって、
それをメインウィンドウに渡しています。



 public partial class App : Application {

  protected override void OnStartup(StartupEventArgs e) {
   base.OnStartup(e);

   //最初に作ったWindowがApplication.MainWindowに設定されるので、OptionDialogより先に作る
   var mainWindow = new MainWindow();

   var userInfo = new UserInfo();
   OptionDialog dialog = new OptionDialog(userInfo);
   dialog.ShowDialog();

   mainWindow.Initialize(userInfo);
   mainWindow.Show();
  }
 }




最後にこのIDとパスワードはTimelineまで伝わって
NetworkCredentialクラスを作り
requestに追加することで認証を通しています。



  public Timeline(string timelineURL, string userName, string password) {
   _url = timelineURL;
   _credential = new NetworkCredential(userName, password);
   StatusAdded += (sender, e) => { }; //あとでnullチェックしないですむように
  }


  /// Twitterサーバからデータを取得する
  /// </summary>
  public void Update() {
   //HTTP用の要求と応答
   HttpWebRequest request = HttpWebRequest.Create(_url) as HttpWebRequest;
   //認証情報を付加する
   if (_credential != null) {
    request.Credentials = _credential;
   }
   HttpWebResponse response = request.GetResponse() as HttpWebResponse;



MainWindowViewModelで設定するタイムラインには
ユーザタイムラインとフレンドタイムラインを表示するように変更してみました。



   var list = new List<TimelineViewModel>() {
    new TimelineViewModel("Friends",
     new Timeline("http://twitter.com/statuses/friends_timeline.json", _userInfo.UserName, _userInfo.Password)),
    new TimelineViewModel( "User",
     new Timeline(string.Format("http://twitter.com/statuses/user_timeline/{0}.json",_userInfo.UserName))),
   };




認証が失敗したときとかの処理を全然していませんが、今回はこれまで。
コードはこちら。
http://wtwitter.codeplex.com/SourceControl/changeset/view/39193

2010年1月24日日曜日

第7回(後編) 複数のタイムラインを表示する

前編からの続きです。

ViewModelを説明します。
(ちなみに、TimelineItemViewModelの方は変更ありません。)



 class TimelineViewModel {
  private ObservableCollection<TimelineItemViewModel> _allItems = new ObservableCollection<TimelineItemViewModel>();
  private DispatcherTimer _timer = new DispatcherTimer();
  private Timeline _timeline;

  /// <summary>
  /// コンストラクタ
  /// </summary>
  /// <param name="displayName">ビューに表示するときの名前</param>
  /// <param name="target">ViewModelが表示対象とするタイムライン</param>
  public TimelineViewModel(string displayName, Timeline target) {
   _displayName = displayName;
   _timeline = target;

   //タイムラインにアイテムが追加されたら通知を受け取るように設定する
   _timeline.StatusAdded += this.OnStatusAdded;

   //定期更新のタイマーを設定する
   _timer.Interval = TimeSpan.FromMinutes(1);
   _timer.Tick += this.OnUpdateTimerTicked;
  }

  /// <summary>
  /// ViewModelが対象とするすべてのアイテム
  /// </summary>
  public ObservableCollection<TimelineItemViewModel> AllItems {
   get { return _allItems; }
  }

  private string _displayName;
  /// <summary>
  /// ビューに表示する場合の名称
  /// </summary>
  public string DisplayName {
   get { return _displayName; }
  }

  /// <summary>
  /// タイムラインを最新に更新する(ネットワークにアクセスする)
  /// </summary>
  public void Update() {
   _timeline.Update();
  }

  /// <summary>
  /// Updateのタイマーが発生したときに呼び出されるメソッド
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void OnUpdateTimerTicked(object sender, EventArgs e) {
   Update();
  }

  /// <summary>
  /// 定期更新のタイマーを作動させる
  /// </summary>
  public void StartUpdateTimer() {
   _timer.Start();
  }

  /// <summary>
  /// 定期更新のタイマーを止める
  /// </summary>
  public void StopUpdateTimer() {
   _timer.Stop();
  }

  /// <summary>
  /// タイムラインにStatusが追加されたときに呼び出されるメソッド
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e">追加されたアイテムを保持しているクラス</param>
  public void OnStatusAdded(object sender, StatusAddedEventArgs e) {
   _allItems.Add(new TimelineItemViewModel(e.Item));
  }
 }



タイマーを持ってきましたが、前回から使っていたので、これに関連する部分はすぐわかると思います。



_timeline.StatusAdded += this.OnStatusAdded;




の部分で、前編で説明したEverntHandlerに、
「Statusが追加されたらOnStatusAddedを呼んでくれ」ということを設定しています。

あとは、Viewで表示するときのためにDisplayNameを作っています。

次に、メインウィンドウを表すViewModelを新しく作ります。



 class MainWindowViewModel {
  public MainWindowViewModel() {
  }

  /// <summary>
  /// ViewModelの初期化
  /// </summary>
  public void Initialize() {
   //Modelの作成
   var list = new List<TimelineViewModel>() {
    new TimelineViewModel( "User",
     new Timeline("http://twitter.com/statuses/user_timeline/yuki1090.json")),
    new TimelineViewModel("Public",
     new Timeline("http://twitter.com/statuses/public_timeline.json"))
   };

   //作成したModelの追加と作動開始
   foreach (var vm in list) {
    Timelines.Add(vm);
    vm.Update();//initialize時に1回更新する
    vm.StartUpdateTimer();
   }
  }

  private ObservableCollection<TimelineViewModel> _timelines = new ObservableCollection<TimelineViewModel>();

  /// <summary>
  /// MainViewに表示するすべてのタイムライン
  /// </summary>
  public ObservableCollection<TimelineViewModel> Timelines {
   get { return _timelines; }
  }
 }




試しに自分のタイムラインと、Publicタイムラインを表示するようにしてみました。
まずInitialize()では2つのTimelineを作って、これを保持します。動作も開始させます。
Viewに対してはTimelinesですべてのタイムラインを公開します。

Viewに当たるWindow1クラスは以下のようになります。



 /// <summary>
 /// Window1.xaml の相互作用ロジック
 /// </summary>
 public partial class Window1 : Window {
  MainWindowViewModel viewModel;
  public Window1() {
   viewModel = new MainWindowViewModel();
   this.DataContext = viewModel;

   InitializeComponent();
  }

  private void Window_Loaded(object sender, RoutedEventArgs e) {
   viewModel.Initialize();
  }
 }


こっちはここまで単純になりました。
DataContextはMainWindowViewModelになりました。
コンストラクタではViewModelを作成して、
ウィンドウがLoadされたときにViewModelを初期化するだけです。
※ViewModelはInitialize()はViewのコンストラクタにいれて、
LoadedはViewModelの動作開始みたいな作りにした方がいいかも、
とこれを書いている時点で思いました。

最後にWindowのXAML



<Window x:Class="WTwitter.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="WTwitter" Height="300" Width="300" 
 Loaded="Window_Loaded">
 <TabControl ItemsSource="{Binding Path=Timelines}">
  <TabControl.ItemTemplate>
   <DataTemplate>
    <TextBlock Text="{Binding Path=DisplayName}"/>
   </DataTemplate>
  </TabControl.ItemTemplate>
  <TabControl.ContentTemplate>
   <DataTemplate>
    <ListBox ItemsSource="{Binding Path=AllItems}"
     ScrollViewer.HorizontalScrollBarVisibility="Disabled"
     HorizontalContentAlignment="Stretch">
     <ListBox.ItemTemplate>
      <DataTemplate>
       <Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="5" Margin="1">
        <DockPanel>
         <Image Source="{Binding Path=ProfileImageUrl}" Width="32" Height="32" DockPanel.Dock="Left"/>
         <TextBlock Text="{Binding Path=Name}" DockPanel.Dock="Top"/>
         <TextBlock Text="{Binding Path=Text}" TextWrapping="Wrap"/>
        </DockPanel>
       </Border>
      </DataTemplate>
     </ListBox.ItemTemplate>
    </ListBox>
   </DataTemplate>
  </TabControl.ContentTemplate>
 </TabControl>
</Window>



大きな構造として外側をTabControlで包みました。

でもItemTemplateがタブの上部を表していて、
ContentTemplateが下のメイン領域を表していることがわかればあとは簡単だと思います。

ItemTemplateではViewModelに新しく作ったDisplayNameを表示する用にしています。
ContentTemplateは前回とほとんど変わっていません。
タブの項目としてMainWindowViewModelに登録されている
Timeline(つまりTimelineViewModel)の数だけ表示されて、
タブを押したときの中身はその選択されたTimelineViewModelに登録されている
AllItemsが表示されます。

ここまでのソース

http://wtwitter.codeplex.com/SourceControl/changeset/view/38815

第7回(前編) 複数のタイムラインを表示する

今回は複数のタイムラインを表示するようにします。

仕上がりイメージ↓(例によって文章は一応ぼかしています)


まず、今回の機能に必須ではないのですが、
前回の作りがひどかったなと思って作り直します。
すみません。

ViewModelが通信してデータをとってくるのはやっぱりおかしいですよね、
ということで、タイムラインを管理するTimelineクラスを作ります。

このクラスはあらかじめTimelineのURLをコンストラクタに与えておき
Update()を呼び出すとタイムラインのデータを取得します。
また、新しいつぶやき(Status)があったらイベントStatusAddedを発生させます。




namespace WTwitter.Model.Twitter {
 class StatusAddedEventArgs : EventArgs {
  private Status _item;
  public StatusAddedEventArgs(Status item) {
   _item = item;
  }

  /// <summary>
  /// 追加されたアイテム
  /// </summary>
  public Status Item {
   get { return _item; }
  }
 }

 /// <summary>
 /// タイムラインを管理するクラス
 /// </summary>
 class Timeline {
  private string _url;
  private List<Status> _allItems = new List<Status>();

  /// <summary>
  /// コンストラクタ
  /// </summary>
  /// <param name="timelineURL">タイムラインを取得するためのURL</param>
  public Timeline(string timelineURL) {
   _url = timelineURL;
   StatusAdded += (sender, e) => { }; //あとでnullチェックしないですむように
  }

  /// <summary>
  /// Statusが追加されたことを通知するイベント
  /// </summary>
  public event EventHandler<StatusAddedEventArgs> StatusAdded;

  /// <summary>
  /// タイムラインに含まれるすべてのアイテム
  /// </summary>
  public List<Status> AllItems {
   get { return _allItems; }
  }

  /// Twitterサーバからデータを取得する
  /// </summary>
  public void Update() {
   //HTTP用の要求と応答
   HttpWebRequest request = HttpWebRequest.Create(_url) as HttpWebRequest;
   HttpWebResponse response = request.GetResponse() as HttpWebResponse;

   //Json形式でデータを取得する
   DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<Status>));
   var stream = response.GetResponseStream();
   var result = serializer.ReadObject(stream) as List<Status>;

   response.Close();

   //とってきたアイテムを、取得済みでない場合は追加する
   foreach (var item in result) {
    if (!_allItems.Contains(item)) {
     _allItems.Add(item);
     StatusAdded(this, new StatusAddedEventArgs(item));
    }
   }
  }
 }
}





コードには2つのクラスが入っています。
以前のViewModelの流用部分が多いので、
EventHandlerの仕組みさえ知っていれば簡単だと思います。

EventHandlerは、主に外のクラスがこの変数にメソッドを登録しておくことで、
イベントの発生時に登録していたメソッドを呼び出してもらう仕組みです。
その際にEventArgsを継承したクラスを介して情報を渡します。
一般的な仕組みなので、詳しくはEventHandlerやdelegateなどのキーワードで調べてください。


前半のStatusAddedEventArgsはその情報を渡すためのクラスです。
新しいStatusがあったときにそのStatusを保持して、イベントに渡されます。

TimelineクラスはURLを保持していて、Update()が呼ばれるたびにURLにアクセスして
新しいStatusがあったらイベントを発生させます。

StatusAdded += (sender, e) => { }



の部分ですが、これは、
EventHandlerは複数のメソッドを登録できるのですが、
(↑つまりイベントが発生したときに複数箇所で通知を受け取ることができる)
登録がない可能性がある場合は呼び出しのたびにnullチェックをしないといけません。
このnullチェックを省略するために、何もしないメソッドを登録しています。
矢印みたいなのはラムダ式というC# 3.0(かな?)で新しく追加された書き方です。
通常


private void XXXEvent(object sender, EventArgs e)


みたいなメソッドをどこかに用意しないといけないのですが、
ラムダ式で匿名関数とすることで、その場で書けます。
(sender, e)が引数を表していて、中括弧の中がメソッドの処理(今回は空)を表します。
引数の型はStatudAdded変数の型から推測可能なので、上記のように省略できます。

地味に変更しているのが、_allItems.Contains()ですでに登録済みかどうかをチェックするところです。
実際は同じ内容のStatusだったとしても、別のタイミングでnewしたものはコンピュータは同一だと判断できないので、
Statusクラスに以下を追加して、同一である判断をIDでするようにします。



  #region IEquatable<Status> メンバ

  public bool Equals(Status other) {
   if (other == null) {
    return false;
   }

   return this.Id == other.Id;
  }

  #endregion

  public override int GetHashCode() {
   return this.Id.GetHashCode();
  }



Equalsを書いたらGetHashCodeも書かないといけません。
ここはこれ以上今回説明しないので他で調べてください。

長くなりそうなので後編に続きます。

2010年1月21日木曜日

第6回 定期的に更新する

Twitterクライアントらしく一定間隔ごとに更新できるようにしてみたいと思います。

まずTimelineViewModelのLoad()メソッドを書き換え。



   //取得した結果をCollectionに格納(すでに入っていたらいったん空にして入れ直す)
   if (_allItems.Count > 0) {
    _allItems.Clear();
   }
   foreach (var item in result) {
    _allItems.Add(new TimelineItemViewModel(item));
   }



Load()の一部を書き換えて、このメソッドを呼ぶたびにAllItemsが最新になるようにしました。
本来ならば、前回からの差分だけを要求して
差分を追加するようにすべきですが、今回は手抜きです。

次にWindow1.xaml.csを以下のように変更します。



namespace WTwitter {
 /// <summary>
 /// Window1.xaml の相互作用ロジック
 /// </summary>
 public partial class Window1 : Window {
  TimelineViewModel viewModel = new TimelineViewModel();
  public Window1() {
   var timer = new DispatcherTimer();
   timer.Interval = TimeSpan.FromMinutes(2);
   timer.Tick += this.UpdateTimeline;
   timer.Start();

   this.DataContext = viewModel;

   InitializeComponent();
  }

  public void UpdateTimeline(object sender, EventArgs e) {
   viewModel.Load();
  }
 }
}




まずviewModel.Load()をイベントハンドラ作ってそこに移しました。
イベントハンドラとは、何かのイベントが起きたときの処理を記述したメソッドです。
今回はTimerの時間で発生するイベントとしてUpdateTimelineを指定しています。
イベントハンドラは以上のようにobjectとEventArgsの引数を取り、
戻り値はvoidというようなパターンが決められています。

あとは簡単だと思います。

今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/38373

第5回 画像を表示する

画像が表示されないと寂しいので発言者のアイコンを表示するようにしてみます。

ぼかしていますが、完成イメージはだいたいこんな感じ↓


まずStatusクラスとUserクラスでこれまで取得していなかったデータをとるようにしてみます。


namespace WTwitter.Model.Twitter {
/// <summary>
/// TwitterのJson形式データ
/// </summary>
[DataContract]
class Status {
[DataMember(Name = "id")]
public long Id { get; set; }
[DataMember(Name = "text")]
public string Text { get;set;}
[DataMember(Name = "favorited")]
public bool Favorited { get; set; }
[DataMember(Name="user")]
public User User { get; set; }
[DataMember(Name = "in_reply_to_user_id")]
public long? InReplyToUserId { get; set; }
[DataMember(Name = "in_reply_to_status_id")]
public long? InReplyToStatusId { get; set; }
[DataMember(Name = "in_reply_to_screen_name")]
public string InReplyToScreenName { get; set; }
public override string ToString() {
return User.ScreenName + ":" + Text;
}
}
}




namespace WTwitter.Model.Twitter {
/// <summary>
/// TwitterのJson形式のユーザ情報
/// </summary>
[DataContract]
class User {
[DataMember(Name = "id")]
public long Id { get; set; }
[DataMember(Name = "screen_name")]
public string ScreenName { get; set;}
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "url")]
public string Url { get; set;}
[DataMember(Name = "description")]
public string Description { get; set; }
[DataMember(Name = "profile_image_url")]
public string ProfileImageUrl { get; set; }
[DataMember(Name = "following")]
public bool? Following { get; set; }
}
}




TwitterのJSON形式にはもっといっぱいデータが入っていますが、
今回は近いうちに使いそうなのだけピックアップしています。
また、すぐに使うのはProfileImageUrl だけです。

データが多くなっただけでそれほど難しくないと思います。
整数が返ってくるものはlong型など、true/falseで返ってくるものはbool型で宣言します。
文字列だけではなく整数型(long)なども正しく型指定しておけば
勝手に文字列から変換してくれます。
ただし変換できない型で変数を宣言していたら例外で落ちます。
また、値が入っていない(null)可能性があるものには
long?のように?を付けてnull許容型で宣言します。



namespace WTwitter.ViewModel.Twitter {
 /// <summary>
 /// タイムラインで表示する1アイテムを表すViewModel
 /// </summary>
 class TimelineItemViewModel {
  private Status _item;
  public TimelineItemViewModel(Status item) {
   _item = item;
  }

  public string Text {
   get { return _item.Text; }
  }

  public string Name {
   get { return _item.User.ScreenName; }
  }

  public string ProfileImageUrl {
   get { return _item.User.ProfileImageUrl; }
  }

  public override string ToString() {
   return _item.ToString();
  }
 }
}



TimelineItemViewModelは今回使うProfileImageUrl だけを増やしました。



<Window x:Class="WTwitter.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="WTwitter" Height="300" Width="300" >
 <DockPanel>
  <ListBox ItemsSource="{Binding Path=AllItems}"
     ScrollViewer.HorizontalScrollBarVisibility="Disabled"
     HorizontalContentAlignment="Stretch">
   <ListBox.ItemTemplate>
    <DataTemplate>
     <Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="5" Margin="1">
      <DockPanel>
       <Image Source="{Binding Path=ProfileImageUrl}" Stretch="None" DockPanel.Dock="Left"/>
       <TextBlock Text="{Binding Path=Name}" DockPanel.Dock="Top"/>
       <TextBlock Text="{Binding Path=Text}" TextWrapping="Wrap"/>
      </DockPanel>
     </Border>
    </DataTemplate>
   </ListBox.ItemTemplate>
  </ListBox>
 </DockPanel>
</Window>



Viewは上記のように、Borderの中をDockPanelに変更しました。
これは今までも使っていましたが、
まずImageを表示するだけ左詰めで領域を確保して、
残りの右の領域の上詰めでNameを表示するTextBlock、
その下にTextを表示するようにしています。

DockPanel.Dock="Left"のような表記が、どっちに詰めるかを表しています。
DockPanelの動作に関しては検索してもらうとわかりやすいページがすぐに出てくると思います。

ここで注意して欲しいのは DockPanel.Dockはすぐ外側のDockPanelのDockプロパティを表します。
さらに外側のDockPanelには関係ありません。
また、Imageのプロパティではなく、DockPanelのプロパティであることを知っておくといいかもしれません。
なのでImageクラスの仕様を調べてもこのようなプロパティは存在しません。
ImageがDockPanelに自分のDock位置はTopですよーと伝えるような感じです。
すぐに知りたい場合は依存関係プロパティという仕組みを調べてみてください。


今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/38263

2010年1月20日水曜日

第4回 Viewをそれっぽくする

今回は貧相だった見た目を少しだけ改善します。
変更するのはWindow1.xamlです。
Window1という名前はViewが増えたらそのうち変更します。



<Window x:Class="WTwitter.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="WTwitter" Height="300" Width="300" >
 <DockPanel>
  <ListBox ItemsSource="{Binding Path=AllItems}"
     ScrollViewer.HorizontalScrollBarVisibility="Disabled"
     HorizontalContentAlignment="Stretch">
   <ListBox.ItemTemplate>
    <DataTemplate>
     <Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="5" Margin="1">
      <StackPanel>
       <TextBlock Text="{Binding Path=Name}"/>
       <TextBlock Text="{Binding Path=Text}" TextWrapping="Wrap"/>
      </StackPanel>
     </Border>
    </DataTemplate>
   </ListBox.ItemTemplate>
  </ListBox>
 </DockPanel>
</Window>




今回変更はこれだけです。

外側から見ていきましょう。
Windowの中にDockpanelがあります。
Dockpanelは中のもの(今回はListbox)を領域いっぱいに表示します。

Listboxは前回同様AllItemsを表示しますが、フォーマットを指定しています。
まずScrollViewer.HorizontalScrollBarVisibility="Disabled"で、
ウィンドウの大きさで折り返すようにしています。
HorizontalContentAlignment="Stretch"は個々のアイテムが
Listboxの幅より小さい場合、Listboxの幅まで広げます。
よくわからない場合はそれぞれのオプションを消してみて実行すると違いがわかると思います。



  <ListBox>
   <ListBox.ItemTemplate>
    <DataTemplate>
     <Border >
     </Border>
    </DataTemplate>
   </ListBox.ItemTemplate>
  </ListBox>



この形は、Listboxを作って
そのListboxのItemTemplateプロパティに
(その中に書いている)DataTemplateを代入する
DataTemplateの中身はBorder以下、ということです。

Borderは枠線です。
Borderの直下には1個しか要素をいれることができないので、
StackPanelをいれます。
StackPanelは中に複数の要素をいれることができて、
デフォルトでは上から順番に積んでいきます。
今回は2つのTextBlockを表示します。
StackPanel自体はレイアウトを決めるもので、表示はされません。
StackPanelの効果がわかりづらい場合はGridにすると、
2つのTextblockが重なって表示されてしまうのがわかると思います。

今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/38200

第3回 少しMVVMっぽく作る

第2回までを書いて、簡潔に説明を書くのは難しいなと思いました。
初心者には十分なほど書いてなくて、中級者以上にはまどろっこしいだけの
『誰得』な中途半端になっているような気も。。

というわけで、少し説明を減らしてキーワードを入れていく感じを目指したいと思います。
キーワードを検索して詳しいサイトを見るとわかりますよ、くらいの。

今回は前回作ったものをMVVMライクに書き換えます。
MVVMは
http://msdn.microsoft.com/ja-jp/magazine/dd419663.aspx
で紹介されているデザインパターンです。

実は私も正しく理解している自信がないのですが、
簡単に説明して、今回この形に近づけます。

MVVMはModel-View-ViewModelの略で
Model=データを扱うところ
ViewModel=ユーザーインターフェイスをモデル化したところ
View=ユーザーインターフェイスそのもの
といったところでしょうか。

プログラミングでは一般的に、データ部分は比較的変更が少なく
ユーザーインターフェイス部分は変更が多いと言われています。
MVVMではView→ViewModel→Modelの一方向の依存関係のみで
逆方向に依存関係はなく、そのため
ユーザーフェイスの仕様変更でモデルまで変更しなければならない、ということが少なくなることが期待できます。
また、ユーザーフェイスが絡むとユニットテストしにくいですが、
ViewModelとModelはViewに依存しないためテストしやすくなります。

もし以上をわかんなくても、なんとなくコードはわかるかと思います。

Modelは前回のStatusとUserクラスをそのまま使います。

ViewModelは一つのタイムラインを表すTimelineViewModelクラスと
そのタイムラインの1つのアイテムを表すTimelineItemViewModelクラスを作ります。



namespace WTwitter.ViewModel.Twitter {
 /// <summary>
 /// タイムラインを表すViewModel
 /// </summary>
 class TimelineViewModel {
  ObservableCollection<TimelineItemViewModel> _allItems = new ObservableCollection<TimelineItemViewModel>();

  /// <summary>
  /// Twitterサーバからデータを取得する
  /// </summary>
  public void Load() {
   //取得するURL
   string url = "http://twitter.com/statuses/user_timeline/yuki1090.json";

   //HTTP用の要求と応答
   HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
   HttpWebResponse response = request.GetResponse() as HttpWebResponse;

   //Json形式でデータを取得する
   DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<Status>));
   var stream = response.GetResponseStream();
   var result = serializer.ReadObject(stream) as List<Status>;

   response.Close();

   //取得した結果をCollectionに格納
   foreach (var item in result) {
    _allItems.Add(new TimelineItemViewModel(item));
   }
  }

  /// <summary>
  /// ViewModelが対象とするすべてのアイテム
  /// </summary>
  public ObservableCollection<TimelineItemViewModel> AllItems {
   get { return _allItems; }
  }
 }
}





namespace WTwitter.ViewModel.Twitter {
 /// <summary>
 /// タイムラインで表示する1アイテムを表すViewModel
 /// </summary>
 class TimelineItemViewModel {
  private Status _item;
  public TimelineItemViewModel(Status item) {
   _item = item;
  }

  public string Text {
   get { return _item.Text; }
  }

  public string Name {
   get { return _item.User.ScreenName; }
  }

  public override string ToString() {
   return _item.ToString();
  }
 }
}



TimelineItemViewModelは_item変数で保持したModelのデータへ仲介するだけです。



TimelineViewModelは表示するTimelineItemViewModelをすべて保持します。
すべてのアイテムをAllItemsプロパティが提供します。
ObservableCollectionクラスは、追加変更が通知されるリストです。
あとで説明するBindingのために追加変更を観察できる必要があるのでこのクラスを使います。

Load()イベントは前回のWindow1クラスのLoadedイベントの内容を持ってきました。
最後に取得データをTextBoxに流し込んでいたところを
AllItemsに追加するように変更しています。
これによって、このViewModelがView(Textbox)を参照しないですむ構造になっていることに注目してください。
ViewModelがViewにデータを渡すのではなくて、
ViewがViewModelを観察して表示に反映するのです。

最後にビューは以下のように簡単になります。



<Window x:Class="WTwitter.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="WTwitter" Height="300" Width="300" >
 <DockPanel>
  <ListBox ItemsSource="{Binding Path=AllItems}"/>
 </DockPanel>
</Window>






namespace WTwitter {
 /// <summary>
 /// Window1.xaml の相互作用ロジック
 /// </summary>
 public partial class Window1 : Window {
  public Window1() {
   var viewModel = new TimelineViewModel();
   viewModel.Load();
   this.DataContext = viewModel;

   InitializeComponent();
  }
 }
}




TextboxをなくしてListboxにしました。
まず.csの方ですが、DataContextは、アバウトに書くと、今このデータを扱っていますよーということです。
TimelineViewModelのデータを作ってこれを入れておきます。

次に.xamlの方ですが、Bindingは、ある要素とデータを関連づけるってことです。
ListboxのItemsSourceをPath=AllItemと関連づけています。
本当は『XXクラスのYYプロパティと関連づける』のように書くのですが、
XXクラスを省略しているのでDataContext(つまりTimelineViewModel)のAllItemsと関連づいています。
ListboxはItemsSourceに指定した要素の個々を順に表示するものですが、
表示の仕方を指定していないので、ToString()した結果を表示しているだけになります。

Bindingは詳しく書くと長くなるので本読んだり検索したりしてください!


今回はView適当なので次回以降作ります。

今回のソース
http://wtwitter.codeplex.com/SourceControl/changeset/view/37990

2010年1月18日月曜日

第2回 データを分解してみる

第1回の説明が少々長くなりすぎた気がしますので、
今回から少し簡潔に書いていきたいと思います。
わからないところは適宜検索したり、参考書などを見たり、質問したりしてください。

第1回で取得したのは1つの長いテキストデータだったので
今回はこれを分解します。
TwitterはXML形式とJSON形式で取得でき、前回はXML形式で取得しましたが、
Search APIとかはXML形式に対応してないので、今回からJSON形式にします。

まず前回のコードのURLの語尾を.jsonに換えて再実行してみてくだだい。
string url =  "http://twitter.com/statuses/user_timeline/yuki1090.json";

すると同じようなデータが異なるフォーマットで得られたと思います。
適当に改行とインデントを入れると次のようになります。



[
{"truncated":false,
  "created_at":"Mon Jan 11 15:26:14 +0000 2010",
・・・略・・・
   "user":{
    "profile_background_color":"1A1B1F",
・・・略・・・
    "screen_name":"yuki1090",
・・・略・・・
   },
・・・略・・・
   "text":"つぶやいた内容のUnicode文字列(読めない)"
},
{"truncated":false,
・・・以下略




{}で囲まれたところが一つのオブジェクトです。
つまり
[
   {つぶやき
       {つぶやいたユーザ情報}
   }
,
   {次のつぶやき
      {つぶやいたユーザ情報}
   }
, ・・・以下繰り返し

の構造になっていることがわかります。
これをクラスで表現します。

前準備として、プロジェクトの「参照設定」を右クリックして、「参照の追加」→「.Netタブ」
・System.ServiceModel.Web
・System.Runtime.Serialization
を追加します。
※Webの方が選択肢に出ない場合は、プロジェクトのプロパティで
対象のフレームワークがClientProfileになっていないか確認してください。
ClientProfileでは出ません。

今回はつぶやいた内容のTextと投稿ユーザ情報のscreen_nameを利用します。
つぶやきを表すStatusクラスとユーザ情報を表すUserクラスを以下のように作ります。




using System.Runtime.Serialization;

namespace WTwitter.Model.Twitter {
 /// <summary>
 /// TwitterのJson形式データ
 /// </summary>
 [DataContract]
 class Status {
  [DataMember(Name = "text")]
  public string Text {
   set;
   get;
  }

  [DataMember(Name="user")]
  public User User {
   set;
   get;
  }

  public override string ToString() {
   return User.ScreenName + ":" + Text;
  }
 }
}






using System.Runtime.Serialization;

namespace WTwitter.Model.Twitter {
 /// <summary>
 /// TwitterのJson形式のユーザ情報
 /// </summary>
 [DataContract]
 class User {
  [DataMember(Name = "screen_name")]
  public string ScreenName {
   get;
   set;
  }
 }
}





StatusクラスがUser情報を持っていると言うことが表現できていると思います。
この情報を使って表示するようにWindow1クラスのWindow_Loadedメソッドを書き直します。



  private void Window_Loaded(object sender, RoutedEventArgs e) {
   //取得するURL
   string url =  "http://twitter.com/statuses/user_timeline/yuki1090.json";
  
   //HTTP用の要求と応答
   HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
   HttpWebResponse response = request.GetResponse() as HttpWebResponse;

   //Json形式でデータを取得する
   DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List));
   var stream = response.GetResponseStream();
   var result = serializer.ReadObject(stream) as List;

   response.Close();

   //取得した結果を1つ1行でTextboxに表示
   foreach (var item in result) {
    ContentTextBox.AppendText(item.ToString() + Environment.NewLine);
   }
  }



今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/37634

ソースだけでわかった人は以下読まなくてもOKです。

■解説

・プログラムのデータをファイルやネットのデータとして送受信するには
データを文字列に変換(シリアライズ)、文字からデータへ復元(デシリアライズ)します。
今回はJson形式で受信したテキストを分解ために
DataContractJsonSerializerを使います。
このクラスを使うと、変換を簡単にできます。
・シリアライザを使うときにはデータ形式を表すクラスに
[DataContract]
の属性を付けます。
またシリアライズ/デシリアライズの対象のクラス変数やプロパティに
[DataMember(Name = "???")]
を付けます。Name=???はJsonのどの部分に相当するかを表します。
・TwitterAPIの仕様のXMLのサンプルを見るとわかりますが、
1つのつぶやきはStatusという名前になっているので今回のクラス名もそうしました。

・Window1クラスの説明
-DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List));
シリアライズ対象がStatusのリストであることを伝えて、インスタンス化します。
-var result = serializer.ReadObject(stream) as List;
ストリームからシリアライザを利用してデータを読み込みます。
 as Listの部分で取得した結果をListに変換して
変数resultに入れています。
-取得したresultには一連のStatusが入っていますので、
それぞれに対してToString()で文字列化してテキストボックスに追加していきます。

2010年1月15日金曜日

第1回 Twitterへアクセスする最小限のプログラムを作ってみる

このblogでは比較的初心者の人でも、
プログラムって面白いということと(思っているよりは)簡単なんだなーということを
感じていただけるように書きたいなぁと思っています。
間違えていることを書いてたらごめんねっ、的なスタンスで書いていきますのでご了承ください。

まず、Visual Studio 2008を用意してください。
無料のExpress Editionで十分です。
というか、それ以外のバージョンでは
以降の説明といろいろと異なるかもしれません。

初回はTwitterのデータをとってくるだけのごく簡単なプログラムを作りたいと思います。
第1回ということで、すごく細かいところまで説明したつもり。

・Visual Studioで新規にプロジェクトを作成してください(WTwitterという名前にしました。)
・Window1.xamlとWindow1.xaml.csがあると思うので
それぞれ以下のように記述してください

Window1.xaml


<Window x:Class="WTwitter.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
 Loaded="Window_Loaded">
 <DockPanel>
  <TextBox x:Name="ContentTextBox"/>
 </DockPanel>
</Window>




Window1.xaml.cs


//デフォルトのusing略

using System.Net;
using System.IO;

namespace WTwitter {
 ///
 /// Window1.xaml の相互作用ロジック
 ///
 public partial class Window1 : Window {
  public Window1() {
   InitializeComponent();
  }

  private void Window_Loaded(object sender, RoutedEventArgs e) {
   string url =  "http://twitter.com/statuses/user_timeline/yuki1090.xml";
   HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
   HttpWebResponse response = request.GetResponse() as HttpWebResponse;

   StreamReader reader = new StreamReader(response.GetResponseStream());
   string content = reader.ReadToEnd();

   reader.Close();
   response.Close();

   ContentTextBox.Text = content;
  }
 }
}




これだけでビルドして実行すると
以下のような画面が表示されると思います。



Unicodeなので日本語は変に表示されますが、
英語のところとか読むとちゃんとデータをとってきているのがわかると思います。
自分のデータをとってくるにはソースコードの中のurlの文字列を自分のIDに変更してください。

今回のプログラムは以下のサイトを参考にさせていただきました。

簡単な解説
■Window1.xaml
~.xamlはウィンドウの配置などを指定するファイルです。
今回書いたのは、ウィンドウ領域全体の大きさのテキストボックスをおいて、
ウィンドウが読み込まれたときにWindow_Loadedが呼ばれるようにしているだけです。
・Loadedを指定することで、ウィンドウが読み込まれたときに呼び出されるメソッドを指定しています。
・デフォルトで入力しているに変更しました。
 DockPanelにすると、中のもの(今回はTextBox)を領域に最大化します。
・TextBoxは~.cs(コードビハインドと呼ばれます)からアクセスできるように
x:NameでContentTextBoxと名前を付けました。

■Window1.xaml.cs
動作を指定するファイルです。
.xamlファイルでLoadedイベントを作った時に
こちらのWindow_Loadedメソッドは自動で作られたと思います。ここを書きます。
・Webへのアクセスはサーバーへ要求(request)を出して応答(response)を得るだけです
・要求は、URLを指定してCreate()するだけ
・応答は要求のGetResponse()を呼び出すだけ
・応答を要求したらデータが流れてきますのでStreamReaderで読みます。
 Streamというのは、ファイルとかウェブとかいろんなデータを同じような方法で読み書きするための
 便利なクラスという感じです。
・string content = reader.ReadToEnd()で流れてくるデータを文字列として取り出します。
・リソース(今回はResponseやStream)は使わなくなったらClose()します
・最後に取り出した文字列を.xamlで作ったTextBoxにセットしています。
Close()の説明を入れようと思いましたが、これだけは長くなりそうなので略。。

※注意
私自身初心者なので、この連載は最後まで続くことを保証できません。。
また、いろいろと間違ったり紆余曲折すると思います。
わからないところは遠慮なくコメント、twitter等で質問してください。

第1回のソースコード