2010年3月12日金曜日

第23回 フィルタを作る

普通のタイムラインであれば、ストレスになる発言があればフォローを外せばいいのですが、
Search APIを使ったタイムラインだとそうもできないので、
IDでフィルタリングする機能を作ります。


将来的には本文中のNGワードとかタイムラインごとの個別のフィルタにも対応するかもしれませんが、
今回はIDだけで判断します。


まずフィルタの設定情報を保持するクラス。Optionが持ちます。




 public class FilterSetting {
  public FilterTargetType Target { get; set; }
  public List<string> ngList { get; set; }
 }

Optionクラスの中にもいくつか初期化を入れています。

フィルタは、「引っかかったらタイムラインに追加しない」だけなら簡単なんですが、
すでに表示されてしまったものを「このユーザーは非表示」などにして後から消すのはちょっと面倒です。

今回は、フィルタが変更されたことを通知する仕組みをフィルタ自身に入れて、
タイムラインがそれを監視する仕組みにします。


フィルタを行うクラスの抽象クラス。


 /// <summary>
 /// 1種類のフィルタを表すクラス
 /// </summary>
 public abstract class Filter {
  public Filter() {
   FilterAdded += (sender, e) => { };
   FilterRemoved += (sender, e) => { };
  }

  /// <summary>
  /// フィルタ処理をする
  /// </summary>
  /// <param name="target">判定するアイテム</param>
  /// <returns>OKならtrue,NGならfalse</returns>
  abstract public bool Judge(ITimelineItem target);

  /// <summary>
  /// フィルタにNGワードなどが追加がされたことを知らせるイベント
  /// ※実際に何が追加されたかは通知しないが、このイベント後はフィルタされるべきアイテムが増えることを意味する。
  /// ※各タイムラインでフィルタをやり直すなど
  /// </summary>
  public event EventHandler FilterAdded;

  /// <summary>
  /// フィルタからNGワードなど削除がされたことを知らせるイベント
  /// ※実際に何が追加されたかは通知しないが、このイベント後はフィルタされるべきアイテムが減ったことを意味する。
  /// </summary>
  public event EventHandler FilterRemoved;

  protected void OnFilterAdded() {
   FilterAdded(this, EventArgs.Empty);
  }

  protected void OnFilterRemoved() {
   FilterRemoved(this, EventArgs.Empty);
  }
 }



その実装。





 /// <summary>
 /// アイテムのIDに対してフィルタリングするクラス
 /// </summary>
 class NgFilterById : Filter {
  #region static member
  /// <summary>
  /// 共通フィルタのインスタンス
  /// ※共通のフィルタはこのプロパティを通じて設定、取得する
  /// ※シングルトンではないので、タイムラインごとに固有のIDフィルタを持つ場合は別途インスタンス化できる
  /// </summary>
  public static NgFilterById Common { get; private set; }

  static NgFilterById() {
   Common = new NgFilterById(new string[]{});
  }
  #endregion

  #region private member
  private List<string> _ngList;
  #endregion

  /// <summary>
  /// コンストラクタ
  /// </summary>
  /// <param name="ngList">インスタンス化時に登録するNGワードのリスト</param>
  public NgFilterById(IEnumerable<string> ngList) {
   _ngList = new List<string>(ngList);
  }

  #region Public Member
  public void AddNg(string ngWord) {
   _ngList.Add(ngWord);
   OnFilterAdded();
  }

  public void RemoveNg(string ngWord) {
   _ngList.Remove(ngWord);
   OnFilterRemoved();
  }
  #endregion

  #region INgFilter メンバ

  override public bool Judge(ITimelineItem target) {
   if (_ngList.Contains(target.ScreenName)) {
    return false;
   }
   return true;
  }

  #endregion
 }


で、どこにフィルタの処理を入れるかですが、
今回はTimelineViewModelで処理します。
ここは悩んだ点なんですが、今の作りでユーザーに見えるタイムラインに1対1に対応しているのは
ModelではなくてViewModelなので、
タイムラインごとの個別フィルタというのちのちの拡張を考えた場合、
ユーザーに見える単位でフィルタを制御した方がわかりやすいかなと思いました。

というわけで、MainWindowViewModelでフィルタを生成して、
各TimelineViewModelにコンストラクタ経由で渡しています。
(コード略)

TimelineViewModelの処理は以下のようになります。
・アイテムが追加されたときにフィルタを通すように変更
・フィルタが増減したときに、差分のフィルタに引っかかるアイテムを消したり、の処理


  /// <summary>
  /// ソースタイムラインにアイテムが追加されたときの処理
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void OnItemAdded(object sender, ItemAddedEventArgs e) {
   if (CheckFilter(e.Item) == false) {
    return;
   }
   AllItems.Add(new TimelineItemViewModel(e.Item, _timeline.UserInfo));
  }

  /// <summary>
  /// フィルタに変更があった時の処理(フィルタが増えたとき)
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void OnFilterAdded(object sender , EventArgs e) {
   var filter = sender as Filter;

   //新しいフィルタでNGになるアイテムを抽出
   var removeTargets = from item in _allItems
          where filter.Judge(item.Item) == false
          select item;

   //メモ:ToList()で新しいリストにしないと
   //次のforeachがIEnumerableのままになり例外発生
   var targets = removeTargets.ToList<TimelineItemViewModel>();

   foreach (var item in targets) {
    _allItems.Remove(item);
   }
  }

  /// <summary>
  /// フィルタに変更があったときの処理(フィルタが減ったとき)
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void OnFilterRemoved(object sender, EventArgs e) {
   //メモ:とりあえずAddの対称として作ったけど、まだ使っていないので動作未確認

   //既存のフィルタで引っかかっているアイテムを、ModelのアイテムをViewModelのアイテムを比較して抽出
   var filtered = from source in _timeline.AllItems
         where _allItems.Any(item => source.Equals(item.Item)) == false
         select source;
   //フィルタ済みのもので、新しいフィルタリストではNGならないものを抽出
   var addTargets = from item in filtered
        where CheckFilter(item) == true
        select item;
   var targets = addTargets.ToList<ITimelineItem>();

   foreach (var item in targets) {
    _allItems.Add(new TimelineItemViewModel(item, _timeline.UserInfo));
   }
  }

  /// <summary>
  /// すべてのフィルタをチェックする
  /// </summary>
  /// <param name="target">フィルタチェック対象のアイテム</param>
  /// <returns>表示OKなアイテムならtrue、フィルタに引っかかるならfalse</returns>
  private bool CheckFilter(ITimelineItem target) {
   if (_filters.Any(filter => filter.Judge(target) == false)) {
    return false;
   } else {
    return true;
   }
  }

コメントを書いたのでたぶんわかりますよね。。。
from .. where ... selectは前に一度出ていたと思いますが、わからなければLINQというキーワードを調べてください。

あとはTimelineItemViewModelにコマンドSetNgUserCommandを追加。(略)

最後に、Viewに追加。
どこでもいいんですが、とりあえず名前を右クリックしたら出るようにしました。


       <TextBlock DockPanel.Dock="Left">
        <Hyperlink Command="{Binding Path=OpenByBrowserCommand}" CommandParameter="{Binding Path=UserUrl}" >
         <TextBlock Text="{Binding Path=Name}"/>
        </Hyperlink>
        <TextBlock.ContextMenu>
         <ContextMenu>
          <MenuItem Header="このユーザーを_NGに登録" Command="{Binding Path=SetNgUserCommand}"/>
         </ContextMenu>
        </TextBlock.ContextMenu>
       </TextBlock>


オプション画面で管理するUIとか作っていないけど、今回はここまで。
いったん付けたフィルタを削除するには設定のXMLから直接消すしかないです。今んところ。

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


0 件のコメント:

コメントを投稿