2010年3月30日火曜日

番外編 変更点+ComboBoxに画像を表示する

http://wtwitter.codeplex.com/SourceControl/changeset/view/44148真新しいことはあまりやっていないんですけど、
ソースコードはだいぶいじりました。

・アイコンの位置を修正
・タイムラインの一番上のアイテムにスクロールするボタンを追加
・タイムラインの発言のユーザー名による絞り込み
・ソースコード上にユーザーを表すModel、ViewModelを追加
・オプション画面でアカウント情報(API制限残り等)を
・オプション画面でタイムラインの新規作成のインターフェイスを変更
ソースコード上ではTimelineSettingTemplateクラスに設定の生成方法を集める
などなど。

ユーザーの絞り込みにComboBoxを使って、
ComboBoxの中に画像を表示するようにしたので、
それについてひとことだけ書きます。

といっても、タイムラインの発言を表示するときに、
ListBoxのアイテムに画像を載せているのとほぼ同じですが。

TimelineViewModelがユーザーを表すUserViewModelのリストを持っています。
あとは、ViewでComboBoxのItemsSourceに指定して、


    <ComboBox ItemsSource="{Binding Path=AllUsers}" SelectedItem="{Binding Path=SelectedFilterUser, Mode=TwoWay}"
        MinWidth="70" ToolTip="ユーザーで絞り込み"/>



ResourceにDataTemplateをおいて表示方法を指定するだけです。



  <DataTemplate DataType="{x:Type vm:UserViewModel}">
   <StackPanel Orientation="Horizontal">
    <Image Source="{Binding Path=ProfileImageUrl}" Width="16" Height="16"/>
    <TextBlock Text="{Binding Path=ScreenName}"/>
   </StackPanel>
  </DataTemplate>


ComboBoxのそれぞれのアイテムがImageとTextBlockを持ったStatckPanelになります。

2010年3月26日金曜日

番外編 Todoアプリを作ってみた

ちょっと自分好みのTodoアプリがなかったので、
WPF/MVVMで作ってみました。

超シンプルで、とにかくいっぱいTODOを登録していって
Done(やった)かLater(あとまわし)をすぐ選択できるようにしただけです。

ソースコードもシンプルなので解説無しです。
もし興味あれば眺めてみてください。
ただ、これまでの連載を全部読んでいる人は
とりわけ新しい発見はないかもしれません。

http://minitodo.codeplex.com/

2010年3月25日木曜日

第30回 エラーをアイコンで表示する

今回の話題の前に。
なぜかオプションのタイムライン更新間隔を反映させるのを忘れて
2分(固定値)間隔で更新していました。
修正しました。

本題ですが、まずタイムライン取得結果の通知の方法を変えました。
今までは1新規アイテムごとに取得報告をViewModelにあげていましたが、
今回は1回のUpdateリクエストごとに1回の結果に関するすべての情報をレポートします。
以下のクラスを通じてイベントを発行します。


 public enum TimelineReadResult {
  /// <summary>
  /// 新しいアイテムの取得
  /// </summary>
  ReadNewItems,

  /// <summary>
  /// アクセスに成功したが新しいアイテムがなかった
  /// </summary>
  NoNewItems,

  /// <summary>
  /// サーバーやネットワークのエラーなど、(ある程度予期できる)エラーの発生
  /// </summary>
  WebErrorOccurred,

  /// <summary>
  /// 上記以外のエラー発生
  /// </summary>
  UnexpectedErrorOccurred
 }

 /// <summary>
 /// タイムラインにアクセスした結果
 /// </summary>
 public class ReadTimelineEventArgs : EventArgs {
  /// <summary>
  /// 取得に成功した場合のコンストラクタ
  /// </summary>
  /// <param name="newItems"></param>
  public ReadTimelineEventArgs(List<ITimelineItem> newItems) {
   if (newItems.Count > 0) {
    ResultType = TimelineReadResult.ReadNewItems;
   } else {
    ResultType = TimelineReadResult.NoNewItems;
   }
   NewItems = newItems;
  }

  /// <summary>
  /// 例外が発生した場合のコンストラクタ
  /// </summary>
  /// <param name="e">読み込み時に発生した例外(をそのまま渡す)</param>
  public ReadTimelineEventArgs(Exception e) {
   if (e is WebException) {
    ResultType = TimelineReadResult.WebErrorOccurred;
   } else {
    ResultType = TimelineReadResult.UnexpectedErrorOccurred;
   }
   Error = e;
  }

  public TimelineReadResult ResultType { private set; get; }

  /// <summary>
  ///
  /// 例外が発生した場合はnull
  /// </summary>
  public List<ITimelineItem> NewItems { private set; get; }

  /// <summary>
  ///
  /// </summary>
  public Exception Error { private set; get; }
 }

それを生成する部分が以下です。


  /// <summary>
  /// BackgroundWorkerのDoWork
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void getItemsAsync(object sender, DoWorkEventArgs e) {
   //タイムラインからアイテムを取得する
   //※別スレッドで実行される

   try {
    using (var response = WebUtility.GetResponse(_url, "GET", new RequestParameter[] { }, _userInfo))
    using (var stream = response.GetResponseStream()) {
     e.Result = _itemsReader.Read(stream).ToList<ITimelineItem>();
    }
   } catch (Exception exception) {
    e.Result = exception;
   }
  }

  /// <summary>
  /// BackgroundWorkerのCompleted
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void getItemsCompleted(object sender, RunWorkerCompletedEventArgs e) {
   //取得したアイテムを通知する
   //※UIスレッドで実行される

   var exception = e.Result as Exception;
   if (exception != null) {
    OnReadItems(new ReadTimelineEventArgs(exception));
    return;
   }

   var result = e.Result as List<ITimelineItem>;
   var newItems = new List<ITimelineItem>();
   foreach (var item in result) {
    if (!AllItems.Contains(item)) {
     newItems.Add(item);
    }
   }
   OnReadItems(new ReadTimelineEventArgs(newItems));
  }
  #endregion

  #region protected method
  protected void OnReadItems(ReadTimelineEventArgs e) {
   if (e.ResultType == TimelineReadResult.ReadNewItems) {
    AllItems.AddRange(e.NewItems);
   }
   TimelineRead(this, e);
  }
  #endregion

なぜかマニュアル通りに例外がとれないので、getItemsAsyncで例外をすべて取って
getItemsCompletedの方で例外をRunWorkerCompletedEventArgs に入れて発行しています。
例外がなかった場合も同じEventArgsを利用して報告します。

TimelineViewModelでは以下のようにイベントを分解しています


  private void OnItemsAdded(object snder, ReadTimelineEventArgs e) {
   switch (e.ResultType) {
    case TimelineReadResult.ReadNewItems:
     foreach (var item in e.NewItems) {
      if (CheckFilter(item) == true) {
       AllItems.Add(new TimelineItemViewModel(item, _timeline.UserInfo));
      }
     }
     Message.Add(EventType.Progress,
      string.Format("タイムライン({0})の取得完了<新規{1}アイテム>", DisplayName, e.NewItems.Count) );
     IsLatestUpdateFailed = false;
     break;
    case TimelineReadResult.NoNewItems:
     Message.Add(EventType.Progress,
      string.Format("タイムライン({0})の取得完了<新規無し>", DisplayName));
     IsLatestUpdateFailed = false;
     break;
    case TimelineReadResult.WebErrorOccurred:
     Message.Add(EventType.Progress,
      string.Format("タイムライン({0})の取得失敗", DisplayName));
     IsLatestUpdateFailed = true;
     break;
    case TimelineReadResult.UnexpectedErrorOccurred:
     Message.Add(EventType.CriticalError,
      string.Format("タイムライン({0})の取得中に予期しない例外発生", DisplayName),
      e.Error.Message + Environment.NewLine + e.Error.StackTrace);
     IsLatestUpdateFailed = true;
     break;
    default:
     throw new InvalidOperationException();
   }
  }


失敗したかどうかをIsLatestUpdateFailed に入れています。

失敗したときは、タイムライン名が表示されているタブにWarningアイコンを表示しましょう。
これまでどおりあらかじめxamlでアイコンを指定しておき、
MainWindowにテンプレートを作ります。


  <!--タブ部分のテンプレート-->
  <DataTemplate x:Key="HeaderTemplate">
   <StackPanel Orientation="Horizontal">
    <!--取得失敗時のイメージ-->
    <!--エラーがないときは非表示にしている-->
    <Image Source="{DynamicResource WarningImage}" Height="14" Width="14">
     <Image.Style>
      <Style TargetType="{x:Type Image}">
       <Style.Triggers>
        <DataTrigger Binding="{Binding Path=IsLatestUpdateFailed}" Value="False">
         <Setter Property="Visibility" Value="Collapsed"/>
        </DataTrigger>
       </Style.Triggers>
      </Style>
     </Image.Style>
    </Image>
    <!--タイムライン名-->
    <TextBlock Text="{Binding Path=DisplayName}"/>
   </StackPanel>
  </DataTemplate>

表示のON、OFFを先程のプロパティで決めています。
TabControlのItemTemplateを、先程のテンプレートを読むように設定します。


  <!--メインのタイムライン表示部分-->
  <TabControl x:Name="timelines" ItemsSource="{Binding Path=TimelinesViewSource.View}"
     ItemTemplate="{StaticResource HeaderTemplate}" IsSynchronizedWithCurrentItem="True">
  </TabControl>

これでエラーが出たときはアイコンが表示されるはずです。

これまで使ったいた1アイテムごとに取得通知を出していた構造はソースからばっさり削除しました。

それから、ちょっと下機能ですが、タイムラインを下のほうまでいっていた時に、
1ボタンで一番上まで戻れるボタンを作りました。
今まで作ってきたCommandの作り方と違うのが気持ち悪いですが、
TimelineViewこの機能はビューの表示領域を変更するだけなのでVMが関わらなくてもいいかなと思い、
まぁいいかなということで。
TimelineViewにて


  <ToolBarTray DockPanel.Dock="Top">
   <ToolBar>
    <!--一番上までスクロールするボタン-->
    <Button Command="{StaticResource MoveToTopItemCommand}">
     <Image Source="{DynamicResource UpToViewTopImage}"/>
     <Button.ToolTip>一番上へスクロール</Button.ToolTip>
    </Button>
   </ToolBar>
  </ToolBarTray>

StaticResourceにコマンドを作って、CommandBindingではそのコマンドが発生したときに実行するメソッドを指定しています。



  <RoutedCommand x:Key="MoveToTopItemCommand"/>

 </UserControl.Resources>
 
 <UserControl.CommandBindings>
  <CommandBinding Command="{StaticResource MoveToTopItemCommand}" Executed="MoveToTopItemExecuted"/>
 </UserControl.CommandBindings>

コードは簡単です。


  private void MoveToTopItemExecuted(object sender, ExecutedRoutedEventArgs e) {
   if (_timeline.Items.Count > 0) {
    _timeline.ScrollIntoView(_timeline.Items[0]);
   }
  }

これがもともとMSで推奨されているやり方に近いかなと思います。

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

2010年3月22日月曜日

第29回 ステータスバーにメッセージを表示する

前回のスレッド化に伴い例外処理を追加しないといけないのですが、
どうせならエラーの内容を表示したいなと思って、
先にステータスバーを作ることにしました。

単純に作るんならすごく簡単なんですが、
一応MVVM式(と思っている)で作ろうと思いました。
なのでModelから。

単純にメッセージのイベントを飛ばすことと、
ある程度の数のログを持っておくことをやるクラスです。
アクセスしやすいようにstaticクラスにしています。
本格的にイベントログを取りたいのならちゃんとした.netの流儀でやるべきかもしれませんが、
そこまでやんなくてもいいかなと思っているので適当に作っています。


 enum EventType {
  /// <summary>
  /// 単純な進捗メッセージ
  /// </summary>
  Progress,
  /// <summary>
  /// エラー
  /// ※ある程度頻繁に起こりえるもの(リクエストのタイムアウトとか)
  /// </summary>
  Error,
  /// <summary>
  /// 致命的なエラー
  /// ※これはずっと残るようにする予定
  /// </summary>
  CriticalError
 }

 class MessageInfo {
  public EventType Type { set; get; }

  /// <summary>
  /// メッセージの要約(1行に収まる程度)
  /// </summary>
  public string Message { set; get; }

  /// <summary>
  /// メッセージの詳細
  /// (必要に応じて)
  /// </summary>
  public string Detail { set; get; }

  /// <summary>
  /// メッセージが発行された時間
  /// </summary>
  public DateTime Time { set; get; }
 }

 class MessageAddedEventArgs : EventArgs {
  public MessageAddedEventArgs(MessageInfo newItem) {
   Item = newItem;
  }

  /// <summary>
  /// 新しく追加されたメッセージ
  /// </summary>
  public MessageInfo Item { private set; get; }
 }

 /// <summary>
 /// イベントやエラーなどの情報を登録する
 /// 登録された情報はViewModelで拾ってWindowに表示されたり、ログに出力されたりする(予定)
 /// </summary>
 static class Message {
  #region private member
  private static object lockObj = new object();
  private static List<MessageInfo> _messageLog = new List<MessageInfo>();
  private const int _logNum = 200;
  private static List<MessageInfo> _criticalErrorLog = new List<MessageInfo>();
  #endregion

  /// <summary>
  /// メッセージが追加された通知イベント
  /// </summary>
  static public event EventHandler<MessageAddedEventArgs> MessageAdded;

  static Message() {
   MessageAdded += (sender, e) => {};
  }

  /// <summary>
  /// メッセージを追加する
  /// </summary>
  /// <param name="type"></param>
  /// <param name="message">メッセージの簡略表示(1行で収まる程度)</param>
  /// <param name="detail">詳細</param>
  static public void Add(EventType type, string message, string detail) {

   var item = new MessageInfo() {
    Type = type,
    Message = message,
    Detail = detail,
    Time = DateTime.Now
   };

   //複数のスレッドから同時にメッセージが飛んでくるかもしれないのでlockする
   lock (lockObj) {
    _messageLog.Add(item);
    if (_messageLog.Count > _logNum) {
     _messageLog.RemoveAt(0);
    }

    if (type == EventType.CriticalError) {
     _criticalErrorLog.Add(item);
    }
   }

   var handler = MessageAdded;
   handler(null, new MessageAddedEventArgs(item));
  }

  /// <summary>
  /// メッセージを追加する
  /// </summary>
  /// <param name="type"></param>
  /// <param name="message">メッセージの簡略表示(1行で収まる程度)</param>
  static public void Add(EventType type, string message) {
   Add(type, message, "");
  }
 }

どこから呼ばれるかわからないのでlockで排他処理しています。

StatusBarを表すViewModelを作ります。
簡単ですね。イベントが来るたびに文字列を更新するだけです。


 public class StatusBarViewModel : ViewModelBase {
  #region private member
  private string _text;
  private string _recentMessages;
  private List<string> _resentMessagesList = new List<string>();
  private const int _recentNumMax = 10;
  #endregion

  public StatusBarViewModel()
   : base("") {
   Message.MessageAdded += this.OnMessageAdded;
  }

  /// <summary>
  /// 一番最新のメッセージを表す文字列
  /// </summary>
  public string Text {
   set {
    if (_text != value) {
     _text = value;
     OnPropertyChanged("Text");
    }
   }

   get {
    return _text;
   }
  }

  /// <summary>
  /// 最近のメッセージを表す文字列(改行で区切られた1つの文字列)
  /// </summary>
  public string RecentMassages {
   set {
    if (_recentMessages != value) {
     _recentMessages = value;
     OnPropertyChanged("RecentMassages");
    }

   }
   get {
    return _recentMessages;
   }
  }

  private void OnMessageAdded(object sender, MessageAddedEventArgs e) {
   string text = string.Format("[{0}] {1}", e.Item.Time.ToLocalTime().ToString("HH:mm"), e.Item.Message);
   Text = text;

   _resentMessagesList.Add(text);
   if (_resentMessagesList.Count > _recentNumMax) {
    _resentMessagesList.RemoveAt(0);
   }
   RecentMassages = string.Join(Environment.NewLine, _resentMessagesList.ToArray());
  }
 }


Viewも簡単です。
UserControlを作って


<UserControl x:Class="WTwitter.View.StatusBarView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 <StatusBar>
  <StatusBarItem>
   <TextBlock Text="{Binding Path=Text}"/>
   <StatusBarItem.ToolTip>
    <TextBlock Text="{Binding Path=RecentMassages}"/>
   </StatusBarItem.ToolTip>
  </StatusBarItem>
 </StatusBar>
</UserControl>



MainWindowにDataTemplateを追加します。


  <DataTemplate DataType="{x:Type vm:StatusBarViewModel}">
   <vw:StatusBarView/>
  </DataTemplate>


あとはContentPresenterを追加して、
MainWindowViewModelにStatusBarViewModelにアクセスするプロパティを持たせています。

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

関係ないけど、追加する機能どうしようかなーと悩みはじめたので、
他でも宣伝していますが、アンケートをはじめました。
簡単な2問だけなので、よかったらご協力ください。

・ついったークライアントに必須の機能
http://poll.fm/1qn9k
・ついったークライアントに求めるもの
http://poll.fm/1qna2

第28回 マルチスレッド処理する

UIの処理とタイムライン取得の処理が重なったら重くなるのでマルチスレッド化します。
今回は先にソースコードを紹介して、あとで説明します。

TimelineクラスのUpdate()の代わりに
非同期版の以下のメソッドを呼び出すようにします。



  /// <summary>
  /// Update()の非同期版
  /// </summary>
  public virtual void UpdateAsync() {
   _backgroundWorker = new BackgroundWorker();
   _backgroundWorker.DoWork += this.getItemsAsync;
   _backgroundWorker.RunWorkerCompleted += this.getItemsCompleted;
   _backgroundWorker.RunWorkerAsync();
  }


  /// <summary>
  /// BackgroundWorkerのDoWork
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void getItemsAsync(object sender, DoWorkEventArgs e) {
   //タイムラインからアイテムを取得する
   //※別スレッドで実行される
   using (var response = WebUtility.GetResponse(_url, "GET", new RequestParameter[] { }, _userInfo))
   using (var stream = response.GetResponseStream()) {
    e.Result = _itemsReader.Read(stream).ToList<ITimelineItem>();
   }
  }

  /// <summary>
  /// BackgroundWorkerのCompleted
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void getItemsCompleted(object sender, RunWorkerCompletedEventArgs e) {
   //取得したアイテムを通知する
   //※UIスレッドで実行される
   var result = e.Result as List<ITimelineItem>;
   foreach (var item in result) {
    if (!AllItems.Contains(item)) {
     OnReadNewItem(item);
    }
   }
  }


今回の変更はほぼそれだけです。
マルチスレッドの場合はそれに応じた例外処理も必要ですが、
それはまだしていません。

さて、コードは簡単ですが、起こっていることを理解するのは難しいかもしれませんので
少々詳しく説明します。
BackgroundWorkerをすでに知っていてコードだけでわかった人は以下不要です。

ちなみに、私自身もマルチスレッドはあまり書いたことがないので、
正直以下の説明に自信がありません。
信頼性50%以下くらいだと思って読んでください(汗

読んでいる人がスレッドくらいは知っているものとして進めます。
でもすごく簡単に説明すると、スレッドは「処理の流れ」です。
スレッドを2つ作ると2つの処理を同時に走らせることができます。
実際には2つの処理をかなり短い単位に区切って交互に実行して
平行動作しているように見せる場合もあれば、
マルチコアのCPUでは本当に2つの処理が同時に走っているかもしれません。

WPFはプログラム側でスレッドを作らない限り、
2つのスレッドが動いています。
UIスレッドとレンダリングスレッドです。(用語は正しくないかもしれません)
レンダリングスレッドは描画の高速化のため使われているらしく、
あんまり気にしなくて良さそうです。
つまり今まで書いてきたコードは全部UIスレッド上で動いています。
1つのスレッドで動いているということは
1つの処理が滞れば全部滞るということです。

UIスレッドはDispatcherで処理の順番が管理されていて、
例えばClickイベント処理や描画処理などがキューに登録されて、
優先度を考慮して順番に処理されていきます。
その中にTimerイベント呼び出される、タイムラインをtwitterAPIで取ってくる処理も登録されるので、
twitterが重くてAPIが反応を返さない(返すのが遅い)ときは、
UIの更新やClickの反応の処理の順番がなかなか回ってこないことになり、
アプリケーションの反応が鈍かったりフリーズしたりしているように見えることになります。

ということで、twitter APIの反応を待つところを別スレッドにします。

別スレッドで処理するときは主に3つのことを考えないといけません。
まず、一般的なマルチスレッド処理で問題になる
共有リソースへのアクセスの排他処理です。
あるスレッドがタイムラインのアイテムのリストを読み込んでいる最中に
別のスレッドがリストを追加・削除したりするとおかしなことになるかもしれません。
2つめは、WindorsFormアプリケーションやWPFアプリケーションでは、
UIのControl(Window自体やその上のテキストボックスやラベルなど)を作ったスレッドしか
そのControlを更新できません。
つまり、別スレッドを作ってそこでtwitterのデータを読み込んでいる時に、
直接Windowのラベルに進捗状況やエラーメッセージを書くようなことはできません。
3つめは、別スレッドでの例外は元スレッドで直接catchできません。

で、スレッドを使用するにはいろいろな方法がありますが、
今回はBackgroundWorkerを使います。
More Effective C#でもスレッドプールやBackgroundWorkerを使えと書いてありますので、
興味のある方は読んでみてください。

このクラスを使うと、上に書いた3つの問題のうち最初の2つは現時点では
ほとんど何もしないでもあまり問題が起こらなそうです。

ちょっとためしてみたところ、
BackgroundWorkerのDoWorkイベントは新しく作られた別スレッドで実行され、
RunWorkerCompletedは元スレッドで実行されるようです。

最初のコードに戻って、getItemsAsync()は自分のTimelineのデータにしかアクセスしないので、
他に同時に実行されているタイムラインのデータに干渉しません。
getItemsCompleted()の方はアイテム追加のイベントをViewModelのクラスが受け取るので、
MultiSourceTimelineとかで複数のタイムラインからイベントがあがってくると
問題が起こるかなと思いましたが、
元のUIスレッドで実行されるっていうことは、
(タイムラインに要求を出した順番とは違うかもしれませんが)
同時にではなくおそらく反応が戻ってきた順にDispatcherのキューに登録され順次実行されるので、
同時アクセスの問題は起こらないかなと思います。

タイムラインの読込待ちがすごく長くなって、
同じタイムラインの次のTimerでの呼び出しが起こると同時アクセスでおかしなことになるかもしれませんが、
そこまで長引くことはあまりないかとおもい、今回はやっていません。

ちなみにvirtualにして、TwitterTimelineクラスでoverrideしたものも書いています。
また、もともと1つのUpdate()を2つのメソッドに分けたため、
ToList()を使っています。(そうしなければUsingの外でstreamを読んでおかしなことになります)

問題の3つめの例外の件は今回対処していませんので、
処理を書いたらそのうちまたブログで書きます。

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

2010年3月21日日曜日

第27回 OnOff(トグル)できる画像付きボタンを作る

その前に、気がついたら前回作ったBindingGroupのところが動かなくなってしまいました。
今回うまくいかなくていろいろいぢったので、それがいけないのか、
前回のコードの時点でまずかったのか未調査です。
今回のコードでは元に戻しています(→Cancelボタンで抜けてもタイムラインへの変更は反映される)
修正したらまた記事載せます。
すみません。すみません。

今回は、お気に入り機能に対応します。
お気に入りボタンは、お気に入りにしていないときは☆、
お気に入りにしたら★の画像が表示されるボタンにします。
このボタンを押すことでON、OFFを切り替えます。

まず、画像のソースをSkinsフォルダのxamlに追加します。


 <BitmapImage x:Key="FavoriteImage" UriSource="icons/16x16/star_full.png"/>
 <BitmapImage x:Key="FavoriteEmptyImage" UriSource="icons/16x16/star_empty.png"/>



次に、新しくボタン用のクラスを作ります。
これは以前作った画像表示ボタンImageButtonの派生クラスとして作ります。

 /// <summary>
 /// On,Offの状態を保持して、状態にあわせた画像を保持するクラス
 /// </summary>
 class ToggleImageButton : ImageButton {
  private bool _isOn = false;
  private BitmapImage _alternativeImage;

  /// <summary>
  /// コンストラクタ
  /// </summary>
  /// <param name="description">ボタンの機能の簡潔な説明</param>
  /// <param name="command">ボタン押下時のコマンド</param>
  /// <param name="_onImage">Onの画像。Imageプロパティで取得する</param>
  /// <param name="_offImage">Offの画像。AlternativeImageプロパティで取得する</param>
  public ToggleImageButton(string description, ICommand command, BitmapImage _onImage, BitmapImage _offImage)
   : base(description, command, _onImage) {
   _alternativeImage = _offImage;
  }

  /// <summary>
  /// Offの時の画像
  /// </summary>
  public BitmapImage AlternativeImage {
   get { return _alternativeImage; }
  }

  public bool IsOn {
   get {
    return _isOn;
   }
   set {
    if (_isOn != value) {
     _isOn = value;
     OnPropertyChanged("IsOn");
    }
   }
  }
 }


簡単ですね。OnOffの状態と、Offの時の画像を持たせただけです。
このクラス自体はそこまで能動的に動きません。
あとIsOnの変更を伝えるために、もとのImageButtonクラスをViewModelBaseから派生させました。
(OnPropertyChangedを使うため)

TimelineItemViewModelにコマンドを持たせて、
FavoriteのAPIを叩く部分はModelに置きました。
これまでどおりのやり方でAPIを叩くだけなので、特に説明はしません。
実際のAPIのやりとりはFavoritesMethodクラスあたりを観てください。

あとはTimelineViewにDataTemplateを置きます。
通常時のImageをStyleのsetterで指定して、
IsOnがFalseの時のImageをTriggerの中のSetterで指定しています。


  <DataTemplate DataType="{x:Type util:ToggleImageButton}">
   <Button Command="{Binding Path=Command}" ToolTip="{Binding Path=Description}"
     Background="Transparent" BorderBrush="Transparent">
    <Image Width="16" Height="16">
     <Image.Style>
      <Style TargetType="{x:Type Image}">
       <Setter Property="Source" Value="{Binding Path=Image}"/>
       <Style.Triggers>
        <DataTrigger Binding="{Binding Path=IsOn}" Value="False">
         <Setter Property="Source" Value="{Binding Path=AlternativeImage}"/>
        </DataTrigger>
       </Style.Triggers>
      </Style>
     </Image.Style>
    </Image>
   </Button>
  </DataTemplate>


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

2010年3月18日木曜日

第26回 タイムラインの削除に対応する

これまで手を抜いて
タイムラインの追加ができても削除ができない状態だったので、
削除をできるようにします。

半分は以前やったリストの選択の回と同じ内容ですが。。

まずOptionDialog.xamlにて、
アイテムを選択できるように、ItemsControlにしていたところをListBoxに変更します。



     <ListBox x:Name="timelines" ItemsSource="{Binding Path=TimelineSettings}"
        IsSynchronizedWithCurrentItem="True"
        ItemContainerStyle="{StaticResource TimelineSettingItemContainer}"
        HorizontalContentAlignment="Stretch" ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>

リソースで見た目を変更するようにStyleを追加します。
選択時のエフェクトも追加します。


  <Style x:Key="TimelineSettingItemContainer" TargetType="{x:Type ListBoxItem}">
   <Setter Property="Template">
    <Setter.Value>
     <ControlTemplate TargetType="{x:Type ListBoxItem}">
      <GroupBox x:Name="ItemContainer" Header="{Binding Path=TimelineTypeName}" Padding="5">
       <ContentPresenter/>
      </GroupBox>
      <ControlTemplate.Triggers>
       <!--選択アイテムのエフェクト-->
       <Trigger Property="IsSelected" Value="True">
        <Setter TargetName="ItemContainer" Property="BorderThickness" Value="3"/>
        <Setter TargetName="ItemContainer" Property="BorderBrush" Value="DarkGray"/>
       </Trigger>
      </ControlTemplate.Triggers>
     </ControlTemplate>
    </Setter.Value>
   </Setter>
  </Style>

見た目がわかりにくい気もしますが、とりあえずおいておきます。
ControlTemplateに枠を描くようにしたので、
その中に表示される各アイテムのDataTemplateからは枠を削除しています。
あと見た目の関係でちょこちょこいぢっています。

削除コマンドも追加します。


<Button Command="{Binding Path=RemoveTimelineCommand}" CommandParameter="{Binding ElementName=timelines, Path=SelectedItem}">タイムライン削除</Button>

CommandParameterで削除のターゲットを指定しています。
ElementNameで最初に作ったListBoxを指すようにし、
ListBoxのSelectedItemプロパティの値がParameterとして渡されます。

各DataTemplateの中にはBindingGroupが追加されていることに注意してください。


    <StackPanel.BindingGroup>
     <BindingGroup Name="OptionBinding"/>
    </StackPanel.BindingGroup>

BindingGroupにより設定変更の確定、変更のキャンセルを制御していますが、
Listの先までには自動的に伝搬してくれないみたいなので、
(↑つまり以前まではタイムラインのURL書き換えてキャンセルボタンで閉じても反映されていた)
各情報にNameプロパティを追加したBindingGroupを追加することで
同時にCancelなどが伝搬するようにしています。

あとはリストへの追加・削除をキャンセルする方法がわからなかったので、
自前で無理矢理作りました。

OptionViewModelで変更前のリストを持っていて
キャンセルされたら戻しています。


  public void CommitEdit() {
   //特に何もしない
  }

  public void BeginEdit() {
   _timelineSettingsBeforeEdit = new List<TimelineSetting>(TimelineSettings);
  }

  public void CancelEdit() {
   if (TimelineSettings.SequenceEqual(_timelineSettingsBeforeEdit)) {
    return;
   }
   _option.TimelineSettings = new ObservableCollection<TimelineSetting>(_timelineSettingsBeforeEdit);
  }


あとはOptionDialogのイベントから呼び出しています。



  private void Window_Loaded(object sender, RoutedEventArgs e) {
   _target.BeginEdit();
   BindingGroup.BeginEdit();
   PasswordBox.Password = _target.UserInfo.Password;
  }

  private void OkButton_Click(object sender, RoutedEventArgs e) {
   _target.CommitEdit();
   BindingGroup.CommitEdit();
   _target.UserInfo.Password = PasswordBox.Password;
   (this.DataContext as OptionViewModel).Save();
   this.DialogResult = true;
   this.Close();
  }

  private void CancelButton_Click(object sender, RoutedEventArgs e) {
   _target.CancelEdit();
   BindingGroup.CancelEdit();
   this.Close();
  }


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