2010年3月6日土曜日

第20回 会話をピックアップして表示する

TwitterのつぶやきにはIDが振られていて、
誰かのつぶやきに返答すると、どの発言に対する返答かという情報が
in_reply_to_status_idに入っています。

これをたどれば、会話を追うことができます。
今回は、返答するときに、投稿画面にこれまでの流れが表示されるようにしたいと思います。

また、twitterは最新のつぶやきが上に来ますが、
会話をたどるときは古い方が上の方が読みやすいですよね?
というわけで、古い方が上になるようにもできるようにします。

投稿画面の完成イメージ↓


まず、どうやって会話をピックアップするかですが、
(プログラム中の単位としての)Timelineだけで処理すると、
あるTimelineには自分の発言が入ってなかったりして、会話がすべて拾えない可能性があります。
まずTimelineの一覧を取得して、Timelineごとに検索して・・・とやるのもいいのですが、
今回はTimelineのアイテムをキャッシュするクラスを作ります。

次に、ModelでキャッシュするかViewModelでキャッシュするかですが、
ViewModelを再利用しよう、ということでViewModelに置きました。
あとから思い返せば、会話構造を取り出したりするので
Modelに置くのが順当のような気がしますが、
作ってしまった後だし、まだModelの方がいいと確信がもてるわけでもないのでこのまま。。

まずキャッシュを管理するクラス


 class TimelineItemVMCache {
  #region private member
  private static TimelineItemVMCache _instance = new TimelineItemVMCache();
  private List<TimelineItemViewModel> _items = new List<TimelineItemViewModel>();
  private readonly long MaxItemNum = 1000;
  #endregion

  #region Constructor
  private TimelineItemVMCache() {
  }
  #endregion

  #region Property
  /// <summary>
  /// シングルトンのインスタンス
  /// </summary>
  public static TimelineItemVMCache Instance {
   get { return _instance; }
  }
  #endregion

  #region Public Method

  /// <summary>
  /// キャッシュにアイテムを追加する
  /// </summary>
  /// <param name="item">追加するアイテム</param>
  public void Add(TimelineItemViewModel item) {
   if (_items.Contains(item)) {
    return;
   }

   _items.Add(item);

   if (_items.Count > MaxItemNum) {
    _items.RemoveAt(0);
   }
  }

  /// <summary>
  /// キャッシュにアイテムが含まれるかどうかを調べる
  /// </summary>
  /// <param name="item"></param>
  /// <returns></returns>
  public bool Contains(TimelineItemViewModel item) {
   return _items.Contains(item);
  }

  /// <summary>
  /// 返答関係をたどって一連の会話を抽出する
  /// </summary>
  /// <param name="target">起点となるアイテム</param>
  /// <returns>一連のTimelineItemViewModel。引数target自身を含む</returns>
  public IEnumerable<TimelineItemViewModel> GetSequenceOfConversation(TimelineItemViewModel target) {
   TimelineItemViewModel result = null;
   TimelineItemViewModel next = target;

   yield return target;
   do {
    result = GetParentOfConversation(next);
    if (result != null) {
     yield return result;
    }
    next = result;
   } while (result != null);
  }

  /// <summary>
  /// 返答関係にあるアイテムの、元発言の方のアイテムを取得する
  /// </summary>
  /// <param name="target">調べたいアイテム</param>
  /// <returns>targetがあるアイテムの返答である場合には、そのアイテムを返す。targetが返答でない場合はnullを返す</returns>
  public TimelineItemViewModel GetParentOfConversation(TimelineItemViewModel target) {
   if (target.InReplyTo == null) {
    return null;
   }

   return _items.FirstOrDefault(
    item => (target.TypeOfSourceTimeline == item.TypeOfSourceTimeline && target.InReplyTo == item.Id)
    );
  }
  #endregion

 }


会話を抽出するメソッド以外は簡単だと思います。
リストで最新の1000件を持つだけです。
会話を抽出するためにInReplyToというプロパティをModelからViewModelまで引っ張って来ています。
また、Search APIの結果とかと混ざらないように、Typeを識別できるようにしています。
(Search APIにはReply元ID情報が含まれないので、会話をたどれません)
あとは、1つ親をたどるメソッド(一番下のメソッド)と、
それを利用してずっと上まで辿るメソッド(下から2番目のメソッド)です。

あとはMainWindowViewModelでvm.AllItemにアイテムが追加されるごとに
キャッシュに入れるようにしています。


    vm.AllItems.CollectionChanged += (sender, e) => {
     if (e.NewItems != null && e.NewItems.Count > 0) {
      foreach (var item in e.NewItems) {
       TimelineItemVMCache.Instance.Add(item as TimelineItemViewModel);
      }
     }
    };

※今はまだ関係ありませんが、のちのちはAllItemsはprivateメンバにしたいところ。

これで会話を辿れるようになったので、あとは最新を下に表示するようにするだけです。
ここは意見がわかれるところかもしれませんが、
リストの一番下のアイテムにフォーカスを移動するとか、
リストの昇順/降順を変更するとかの処理は、
ViewよりもViewModelで管理した方が便利かと思います。

すでにTimelineView.xamlで使っているCollectionViewSourceが
ビューへの抽象的なアクセス手段を提供してくれます。
というわけで、これをViewModelに移動させます。



  private CollectionViewSource _viewSource;

  public TimelineViewModel(string displayName, IEnumerable<TimelineItemViewModel> initialItems, ListSortDirection sortDirection)
   : base(displayName) {
   _allItems = new ObservableCollection<TimelineItemViewModel>(initialItems);
   _viewSource = new CollectionViewSource();
   _viewSource.Source = _allItems;
   _viewSource.SortDescriptions.Add(new SortDescription("CreatedAt", sortDirection));

特に難しいことはないと思います。コンストラクタでソート方向を渡すので、
ViewModel生成時に昇順/降順を選べます。

次に、ViewからこのCollectionViewSource をバインドするように変更します。
ここで意外と引っかかりましたが、バインディングは以下のようになります。


 <ListBox ItemsSource="{Binding Path=ViewSource.View}"



CollectionViewSource のインスタンス自身ではなく、
CollectionViewSource インスタンスのViewプロパティにバインドします。

最後にTimelineItemViewModelの返答コマンドをちょこっと変更します。
キャッシュから会話を拾い出してコンストラクタへ渡すのと、
TimelineViewModelのコンストラクタにSort方向を渡すようにしました。


  private void OnReplyCommandRequested(object parameter) {
   var references = TimelineItemVMCache.Instance.GetSequenceOfConversation(this).ToList<TimelineItemViewModel>();
   var vm = new SubmitPanelViewModel(_userInfo, this.Item,
    new TimelineViewModel("", references, ListSortDirection.Ascending));
   vm.SetTextToReply();
   Utility.ViewConnector.Instance.Show(vm);
  }



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

0 件のコメント:

コメントを投稿