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

0 件のコメント:

コメントを投稿