2010年8月2日月曜日

番外編:パフォーマンス

バージョンβ0.04公開しました。
http://wtwitter.codeplex.com/
若干のUIの改善とパフォーマンス改善です。
ちなみにソースコードはなんかUPLOADに失敗したのでまだあげていません。

久しぶりの更新ですが、
この間にパフォーマンスについていろんなサイトで調べたことと若干の経験からわかったことを元に
メモ程度に箇条書きしたいと思います。
私自身ではパフォーマンス計測とかの検証はしていないのであしからず。

・WPFの描画は、アニメーションや透過、エフェクトなどを多用しなければそんなに遅くない(※1)
・パフォーマンスのネックになりやすいのはレイアウト
・今のクライアントの作りでは、ListBoxに『可変サイズの』ListBoxItemをたくさんおいているのが一番の原因だったと思われる。
 高さ固定ならば見えない部分のサイズ計測は不要なので仮想化の恩恵にあずかれるが、
 可変だと実際に配置して計算してみないとわからないので遅い。
・レイアウトがネストすると指数関数的?に計算量が増えるので、
逆にレイアウトを簡素化できれば、表示量はあまり減らさなくても大きく軽量化できる。

※1
WPFはGPU(グラフィックボード)を描画に使うので、
それなりに新しいGPUを積んでいれば描画は早い。
ただし、GPUを使うようなソフト(たとえばWindowモードにした3Dゲームなど)を表示していたら
描画は遅くなる。

2010年7月5日月曜日

番外編:MVVMパターンとDMVVMパターン

※今日は小難しい話で今後に関係ないのでスキップしていただいてもOKです。

世の中ではWPFといったらMVVM(Model-View-ViewModel)パターンという認識ができつつあり、
MVVMにどれだけ厳密に従うか、といった話題を目にすることが多くなったような気がします。

・ViewModelからViewへの依存関係は持たない
・ViewはXamlで書いて、コードビハインドにはコードを持たない
・ViewModelはViewを意識した作りにしない

この連載ではMVVMパターンを使っていますが、
そういう意味ではかなりぬるいMVVMパターンです。

厳密に従った方が、特に大規模な多人数開発において
コードの理解性や保守性が向上しますが、
往々にして生産性(コードを書く時間)との両立は難しい場合が多いです。

Modelへの単純なアクセスのためのプロパティを
ViewModelに大量に追加することは、MVVMで作ったら誰しも経験があるかとは思います。
ある意味、単純であってもコード量の増加は理解性の低下といえます。
また、MVVMの厳密性を守るためのコネクタが肥大化・複雑化したら本末転倒です。

ViewModelがUI(View)を意識するかどうかについても(注:依存するかどうかではないです)難しい問題であり、
完全にUIを意識しないViewModelを書けば、Viewは自由にカスタマイズできるようになりますが、
その分Viewで作り込まないといけない部分が増えることが予想されます。
ある程度はUIを意識した作りにしておいて、Viewはデータを単純に表示するだけという状況に近づければ、
Viewにバグが入り込む余地が減り、ViewModelまではUnitTestできるというMVVMの利点を活かせます。
場合によって一長一短ということです。

これらはプロジェクトの特性と与えられた時間などのトレードオフで検討していただきたいです。

また、私がWPFを始めた時は、よいコードとしてminiUMLが挙げられていて、
そのプロジェクトではMVVMではなくDM-V-VM(DataModel-View-ViewModel)が使われています。
これはMVVMに近いのですが、
ViewからViewModelとDataModel両方のアクセスを許し、
また、ViewModelだけではなくDataModelもWPFにべったりの作りとなっており、
その分シンプルで冗長なコードが少なくなっています。
まぁ作図ツールなのでデータとしては単なるXMLで、
Windowsのクライアント上にどう図形を表示するかがキモなので、
当然の判断のような気もします。

私感ですが、データが単純でUIが複雑なアプリケーションはDM-V-VMが、
逆にデータが複雑でUIが単純なアプリケーションはM-V-VMがあうのではないのでしょうか。
もちろん私が知らないだけで、もっといいパターンもあるかもしれません。
また、どれかを選んで厳密に従わなければならない、ということではなく、
そのパターンのいいところを理解して、うまくあわないところは柔軟にしたほうがいいと思うのです。

といわけで、この連載のコードも悪いところもいいところも(?)あると思いますので、
悪いところはまねしないで、工夫して使ってください!<結局のところ今日はこれが言いたかった。
なんか、ネットで「あのソフトのソースコードはこうなっていたので・・・」という発言をちらほら見て
気になったので書いてみました。


DM-V-VMパターンが気になった方はMiniUMLで検索してソースコードを見てみてください。

あと、更新すると言って更新してなくてすみません。
新しいクライアントのイメージができあがらないのです。。

2010年6月10日木曜日

番外編 VisualStateManagerを使ってみる

今回はVisual State Manager(以下VSM)を使ってみます。
VSMは名前の通り、コントロールなどの表示物の状態管理をしやすくするものです。


詳しい経緯は知らないのですが、
もともとはTriggerが貧弱なSilverLight用に用意されたものらしく、
WPFでは必ずしも使わなくても済ませられるようです。
しかしこれを使うことにより、状態の管理、およびモデルとビューの分離がしやすくなるのではと思っています。


これは.Net Framework3.5だけでは使えないので、
3.5にWPF tool kitを導入するか、.Net Frameworkの4.0を導入してください。


今回のサンプルは以下のようなイメージです。

まずは単純に2つの楕円が回転するだけのコードを。


<Window x:Class="VisualStateManager.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
    <Grid>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="45" CenterX="50" CenterY="25" x:Name="_rotate1"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="0" CenterX="50" CenterY="25" x:Name="_rotate2"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Grid.Triggers>
   <EventTrigger RoutedEvent="Loaded">
    <BeginStoryboard>
     <Storyboard BeginTime="0:0:0" RepeatBehavior="Forever">
      <DoubleAnimation Storyboard.TargetName="_rotate1" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="45" To="405"/>
      <DoubleAnimation Storyboard.TargetName="_rotate2" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="360" To="0"/>
     </Storyboard>
    </BeginStoryboard>
   </EventTrigger>
  </Grid.Triggers>
  
  <Grid.Background>
   <RadialGradientBrush Center="0.5, 0.5">
    <GradientStop Color="Gray" Offset="0"/>
    <GradientStop Color="Black" Offset="1"/>
   </RadialGradientBrush>
  </Grid.Background>
 </Grid>
</Window>

2つの楕円(Ellipse)を配置して、
それぞれRotateTransformで角度を付けています。

あとはLoadedイベントでアニメーション(Storyboard)を開始します。
アニメーションはそれぞれ逆方向に
RotateTransformの角度の数値(double値)を2秒で360度変化するように設定しています。
最後のBackgroundは雰囲気を出すためだけのものです。

ではVisualStateManagerを使ってみます。
状態が「通信中」の時だけアニメーションするようにしてみましょう。
「通信中」かどうかの状態変化はチェックボックスで代用します。


<Window x:Class="VisualStateManagerSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">

 <VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
   <VisualState x:Name="Idle">

   </VisualState>
   <VisualState Name="Accessing">
    <Storyboard BeginTime="0:0:0" RepeatBehavior="Forever">
     <DoubleAnimation Storyboard.TargetName="_rotate1" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="45" To="405"/>
     <DoubleAnimation Storyboard.TargetName="_rotate2" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="360" To="0"/>
    </Storyboard>
   </VisualState>
  </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

 <Grid>
  <CheckBox Foreground="White" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked">通信中</CheckBox>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="45" CenterX="50" CenterY="25" x:Name="_rotate1"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="0" CenterX="50" CenterY="25" x:Name="_rotate2"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  
  <Grid.Background>
   <RadialGradientBrush Center="0.5, 0.5">
    <GradientStop Color="Gray" Offset="0"/>
    <GradientStop Color="Black" Offset="1"/>
   </RadialGradientBrush>
  </Grid.Background>
 </Grid>
</Window>


Windowの下にを置きます。
ちなみにのような書き方になっていないのは添付プロパティだからです。
んでそこにAccessingとIdleの2つの状態を作っています。
AccessingのVisualStateに前出のStoryboardをごっそり持って行きます。
これでAccessingになったらStoryboardが動き出すようになります。

あとはコードビハインドで状態を変化させてやるだけです。


  private void CheckBox_Checked(object sender, RoutedEventArgs e) {
   VisualStateManager.GoToElementState(this, "Accessing", false);
  }

  private void CheckBox_Unchecked(object sender, RoutedEventArgs e) {
   VisualStateManager.GoToElementState(this, "Idle", false);
  }


VisualStateManagerはWindowが(添付プロパティとして)持っているので
最初の引数はthisです。

さて、GoToElementStateの最初の引数がControl(=View)なので、
ModelとViewModelがViewを知らないという前提のMVVMモデルでは、
ModelやViewModelから単純にこの操作ができません。

これに対するよい解法が以下のページで紹介されています。
http://tdanemar.wordpress.com/2009/11/15/using-the-visualstatemanager-with-the-model-view-viewmodel-pattern-in-wpf-or-silverlight/

というわけで、まずはViewModelを作ります。
これは今までの連載で何度も書いている形なのでわかると思います。


namespace VisualStateManagerSample {
 class CommunicationViewModel : INotifyPropertyChanged {
  public CommunicationViewModel() {
  }

  private bool _isAccessing;
  public bool IsAccessing {
   get {
    return _isAccessing;
   }
   set {
    if (_isAccessing != value) {
     _isAccessing = value;
     OnPropertyChanged("IsAccessing");
     OnPropertyChanged("State");
    }
   }
  }

  public string State {
   get {
    if (IsAccessing) {
     return "Accessing";
    } else {
     return "Idle";
    }
   }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private void OnPropertyChanged(string propertyName) {
   var handler = PropertyChanged;
   if (handler != null) {
    handler(this, new PropertyChangedEventArgs(propertyName));
   }
  }
 }
}

いつものOnPropertyChangedと、

状態をboolで持っていて、状態を表す文字列Stateが連動しているだけです。

MainWindowのコードビハインドはCheckboxのイベントハンドラが無くなって
以下のようにスッカラカンになります。
上記URLからもらってきたStateManagerをほぼそのままおいています。
※これは別ファイルに書くのがめんどくさかっただけでMainWindowにある必要はありません
唯一書き換えているのはGoToState→GoToElementStateに呼び出しをかえました。
MSDNを見る限りVisualStateManagerをControlTemplateに置くかどうかで使い分けるみたいです。


namespace VisualStateManagerSample {
 /// <summary>
 /// MainWindow.xaml の相互作用ロジック
 /// </summary>
 public partial class MainWindow : Window {
  public MainWindow() {
   InitializeComponent();
  }
 }

 public class StateManager : DependencyObject {
  public static string GetVisualStateProperty(DependencyObject obj) {
   return (string)obj.GetValue(VisualStatePropertyProperty);
  }

  public static void SetVisualStateProperty(DependencyObject obj, string value) {
   obj.SetValue(VisualStatePropertyProperty, value);
  }

  public static readonly DependencyProperty VisualStatePropertyProperty =
   DependencyProperty.RegisterAttached(
   "VisualStateProperty",
   typeof(string),
   typeof(StateManager),
   new PropertyMetadata((s, e) => {
    var propertyName = (string)e.NewValue;
    var ctrl = s as Control;
    if (ctrl == null)
     throw new InvalidOperationException("This attached property only supports types derived from Control.");
    System.Windows.VisualStateManager.GoToElementState(ctrl, (string)e.NewValue, true);
   }));
 }

}


XAMLは以下のようになります。


<Window x:Class="VisualStateManagerSample.MainWindow"
  xmlns:local="clr-namespace:VisualStateManagerSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300"
  local:StateManager.VisualStateProperty="{Binding Path=State}">
 <Window.DataContext>
  <local:CommunicationViewModel/>
 </Window.DataContext>
 
 <VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
   <VisualState x:Name="Idle">

   </VisualState>
   <VisualState Name="Accessing">
    <Storyboard BeginTime="0:0:0" RepeatBehavior="Forever">
     <DoubleAnimation Storyboard.TargetName="_rotate1" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="45" To="405"/>
     <DoubleAnimation Storyboard.TargetName="_rotate2" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="360" To="0"/>
    </Storyboard>
   </VisualState>
  </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

 <Grid>
  <CheckBox Foreground="White" IsChecked="{Binding Path=IsAccessing}">通信中</CheckBox>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="45" CenterX="50" CenterY="25" x:Name="_rotate1"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="0" CenterX="50" CenterY="25" x:Name="_rotate2"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  
  <Grid.Background>
   <RadialGradientBrush Center="0.5, 0.5">
    <GradientStop Color="Gray" Offset="0"/>
    <GradientStop Color="Black" Offset="1"/>
   </RadialGradientBrush>
  </Grid.Background>
 </Grid>
</Window>


DataContextにはViewModelを入れていて、
local:StateManager.VisualStateProperty="{Binding Path=State}"で
ViewModelのStateに応じてVisualStateManagerが動作するようになります。

ViewModelの動作をシミュレートするためにCheckboxを残していますが、
基本的にViewModelが自分の状態を表す文字列を書き換えるだけでよく、
Viewはその文字列に従って状態を表すアニメーションするだけでよい構造になっていることがわかるかと思います。

今回のソースコードはどこにもアップロードしていませんが、
ここに書いている部分で、デフォルトのAppクラスを除いたらすべてです。

2010年5月31日月曜日

番外編 IsSynchronizedWithCurrentItemのメモ

前にどこかにちょっと書いたような気がするけど、メモ。

リストのIsSynchronizedWithCurrentItemは便利でわりと重要だと思うんだけど、
MSDNもエッセンシャルWPFも、現在性管理とか難しい言葉で説明してあるので、
ここではかみ砕いた表現での説明に挑戦します。

個人的には、このプロパティを使うときは2つのパターンのどちらかです。

  1. リストで現在選択中のアイテムを『即時に』取得したい
  2. リストの選択項目と、他の表示コントロールを同期させたい

1は、IsSynchronizedWithCurrentItem=falseの時は
リストからフォーカスが移動したときしかSelectedItemが更新されませんが、
trueにしておくことで、リストの選択項目を変更した瞬間に更新されます。

多くの場合、OKボタンなどを押したときに選択されている項目を取得できればいいのですが、
ショートカットキーとかを駆使したりして、
リストにフォーカスがあるままで細かく選択項目を制御/取得したいときに
trueにすると便利になります。

2はエッセンシャルWPFでも紹介されている、
リストを表示して、リストの選択項目が変わると、
別の領域に選択項目に応じてその項目の詳細を表示するものです。
Master-Detailパターンと呼ばれるものです。

たとえばこんな感じです。
リストボックスにタイムラインの一覧を選択肢としていれておいて、
選択中のタイムラインの説明を下の領域に表示しています。

正直いくつかのサンプルを見ても、どうしてそうなるのかがわからなかったのですが、
たぶん以下のような原理で動いています。
「たぶん」というのが情けないですが。

まず、Window(またはUserControl)のDataContextを
そのままリストのデータとしてバインドしている場合は簡単です。
以下、実際にためしていないのですが、動くはずです。


<UserControl x:Class="Cassador.Twitter.View.Parts.EditDataSource"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
 <DockPanel>
  <ComboBox ItemsSource="{Binding}" DisplayMemberPath="DisplayName"
     IsSynchronizedWithCurrentItem="True" DockPanel.Dock="Top"/>
  <GroupBox Header="説明">
   <TextBlock Text="{Binding Path=Description}" TextWrapping="Wrap"/>
  </GroupBox>
 </DockPanel>
</UserControl>

選択中のアイテムのDescriptionプロパティをTextBlockに表示します。

ですが、MVVMで開発しているとWindowのDataContextはViewModelで
ViewModelのプロパティにリストを持っていて、それにバインドする場合が多いと思います。
ですが、以下のようにすると、動きません(これもためしていませんが、きっと。)


<UserControl x:Class="Cassador.Twitter.View.Parts.EditDataSource"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
 <DockPanel>
  <ComboBox ItemsSource="{Binding Path=DataSourceTemplates}" DisplayMemberPath="DisplayName"
     IsSynchronizedWithCurrentItem="True" DockPanel.Dock="Top"/>
  <GroupBox Header="説明">
   <TextBlock Text="{Binding Path=Description}" TextWrapping="Wrap"/>
  </GroupBox>
 </DockPanel>
</UserControl>

理由は、TextBlockが見ているのはWindowのDataContext(のしたのDescription)であり
リストが操作しているのは前の例と違ってWindowのDataContextではないからです。

これを解決する方法はいくつかあると思いますが、
1つはTextBlockのDataContextをComboBoxとあわせてやることです。
以下実際に使っているコード。



<UserControl x:Class="Cassador.Twitter.View.Parts.EditDataSource"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" >
 <DockPanel>
  <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
   <Button Command="{Binding Path=CloseCommand}">_Cancel</Button>
   <Button>_OK</Button>
  </StackPanel>
  <DockPanel DataContext="{Binding Path=DataSourceTemplates}">
   <ComboBox ItemsSource="{Binding}" DisplayMemberPath="DisplayName"
       IsSynchronizedWithCurrentItem="True" DockPanel.Dock="Top" Name="listbox"/>
   <GroupBox Header="説明">
    <TextBlock Text="{Binding Path=Description}" TextWrapping="Wrap"/>
   </GroupBox>
  </DockPanel>
 </DockPanel>
</UserControl>




DataContextをあわせるために、DockPanelを1枚かませています
(DataContextを持たせられれば他のコントロールでもいいと思います)
上記の例の場合、ボタンのCommandのはリストの選択項目とは無関係の
DataContextのCloseCommandにバインディングしていることに注目してください。
ボタンを内側のDockPanelの中に持ってくると、
それはリストの選択項目のインスタンスのCloseCommandにバインドされるということになります。
(そして選択項目にCloseCommandがなければもちろん動きません)

2010年5月30日日曜日

番外編 MEFを使ってみる

MEFは、プラグインなどのあとから拡張できる構造を
簡単に実現することができる仕組みだと認識しています。
以前はプラグインを実行時に読み込むには、
リフレクションを使ってそれなりのコードを書かないといけなかったのですが、
MEFによりだいぶ単純になっていて、また柔軟さを持っているようです。
※使いこなしていないので断言できません

本当はこれまでのように実際のコードで例を示したかったのですが、
複雑になりそうなのでサンプルコードで示します。

まず一つのクラス内でむりやり書いたコードです。

一応twitterクライアントで使えそうなシチュエーションということで、
タイムラインを表すITimelineインターフェイスと、
その具体的な実装PublicTimelineクラスとFriendTimelineクラスがあります。

そしてExport属性を付けたクラスを
ImportまたはImportMany属性を付けたプロパティが受け取ります。

コンストラクタでやっていることは、
CatalogクラスがExportを探してくる役目、
CompositionContainerがExportとImportの参照関係を構築する役目、
といったところです。(たぶん)



using System.ComponentModel.Composition;
using System.Reflection;
using System.ComponentModel.Composition.Hosting;


 public partial class MainWindow : Window {
  public MainWindow() {
   DataContext = this;
   var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
   var container = new CompositionContainer(catalog);
   container.ComposeParts(this);
   InitializeComponent();
  }

  public interface ITimeline {
   string Name { get; }
  }

  [Export(typeof(ITimeline))]
  class PublicTimeline : ITimeline {
   public string Name {
    get { return "みんなのタイムライン"; }
   }
  }

  [Export(typeof(ITimeline))]
  class FriendTimeline : ITimeline {
   public string Name {
    get { return "友達のタイムライン"; }
   }
  }

  [ImportMany]
  public IEnumerable<ITimeline> AllTimelines {
   get;
   set;
  }
 }

もちろん実際Exportするのははインナークラスである必要はありません。

ためしにViewを以下のように書いたらタイムライン名が表示されるはずです。


<Window x:Class="MEFSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
  <ListBox ItemsSource="{Binding Path=AllTimelines}" DisplayMemberPath="Name"/>
 </Grid>
</Window>

使う側(Importする側)でITimelineの実装クラスを列挙する必要がないことに注目してください。
また、タイムラインの実装クラスを増やしたい場合はExportがついたクラスを増やすだけです。
この場合も使う側(Importする側)に変更がいりません。

ただこの方法では、同じアセンブリからしかImportできません。
他のファイル(.dll)からImportするには、DirectoryCatalogを使います。
上のサンプルとは全く関係ありませんが、
作成中のアプリのコードで示すと、
Pluginディレクトリのなかのすべてのdllファイルの中の
IServiceインターフェイスの実装を探すコードは
たとえば以下のような感じになります。


   using (var dirCatalog = new DirectoryCatalog("Plugins"))
   using (var container = new CompositionContainer(dirCatalog)) {
    _services = container.GetExportedValues<IService>();
   }



(1例目はMSDNのサンプルがそうなっていたのでやっていませんでしたが、
Dispose()があるので実際のコードの2例目はusingで囲っています。)

これで、「プラグインはIServiceを実装してPluginディレクトリに入れる」というルールを決めるだけで
アプリケーション側はプラグインの一覧を簡単に探すことができます。
実用にはもっといろいろと決めることがありますが、
基本はこんなところです。

他にもCatalogの種類はあるし、いろんな呼び出し方がありそうなので、
詳しくは仕様を調べてください。
クラスではなくてメソッドをインポートする、などできます。
インポートに条件を付けたりもできます。
たくさんページがありますが、公式では以下のページが比較的わかりやすいかなと思いました。
http://msdn.microsoft.com/ja-jp/magazine/ee291628.aspx

2010年5月25日火曜日

番外編 拡張メソッドを使ってみる

ちょっと新しく作り直しているところがいろいろ躓いているので、
しばらく小ネタでつないでみたいと思います。
トピック作ってまで書くほどことではないかもしれませんが、
他に書くことがないので。。

今日使ってみるのは『拡張メソッド』。
既存のクラスにメソッドを追加できます。
といっても、クラスの内部(privateメンバとか)にアクセスできないので、
クラスにメソッドが『追加されたかのように見せる』機能のイメージですね。

この機能は以下の2つの理由であまり多用しない方がいいような気がします。
・本来のクラスと別の場所にコードがあるので、探しにくくなる
・本来持っていないメソッドが存在するようになるので、
 この機能に慣れてない人が、拡張メソッドが使われているところを読むと『???』となる可能性がある


ですが使い方によっては、ちょっとした手間でコードを読みやすくできそうです。


たとえば、以下のようなコードがあったとします。


if (string.IsNullOrEmpty(filename)) {


ここで以下のような拡張メソッドを作ると


 public static class StringExtension {
  public static bool IsNullOrEmpty(this string text) {
   return string.IsNullOrEmpty(text);
  }

以下のように書けます。


if (filename.IsNullOrEmpty()) {


後者の方が英語としてすんなり読めますよね?

このように、stringのような自分では手を加えられないクラスに、
拡張メソッドによってメソッドを追加することにより、
読みやすいコードを書くことができるようになります。

まぁ人によってはあんまり効果があると思わないかもしれませんが、
ロジックが複雑になってif分の中がごちゃごちゃしてくるほど
この若干の読みやすさの差がだいぶ効いてくるような気がします。

今後の記事で時々出てくるかもしれないので紹介してみました。

2010年5月17日月曜日

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

http://minitodo.codeplex.com/

ついったークライアントが行き詰まっているので、Todoアプリを更新してみました。
今回はアニメーションの説明でも。
ソースが必要な場合は上のリンクから取ってね。

最初に断っておきますが、MVVMとアニメーションは相性が悪いらしいです。
http://blog.sharplab.net/computer/cprograming/wpf/3065/
の記事とか読んだ限り。
そして今回の私の投稿はMVVMを思いっきり無視して作っています。
むしろ邪道なやり方と言ってもいいかもしれません。
ちゃんとやりたい方は上記のリンク先のやり方とかやってください。

そして本流のついったークライアントではないので、かなり省いて説明します。
実際の動きがイメージできないときはバイナリをDLしてためしてください。

まず、Todoを追加したときに、
右からスライドインしてくるようなアニメーションを作ります。

XAML


       <!--新規作成時のスライドインアニメーション用-->
       <Grid.RenderTransform>
        <TranslateTransform x:Name="_slideInTransform"></TranslateTransform>
       </Grid.RenderTransform>
       <Grid.Resources>
        <!--新規アイテムを挿入するアニメーション-->
        <Storyboard x:Key="_slideInAnimation">
         <DoubleAnimation Storyboard.TargetName="_slideInTransform"
                 Storyboard.TargetProperty="X"
                 From="{Binding ElementName=_window, Path=Width}" Duration="0:0:0.5">
         </DoubleAnimation>
        </Storyboard>

Gridは1つのアイテムを表す入れ物です。(ListBoxItemの直下)
GridにTranslateTransformをつけておいて
(プロパティを付けていないので、おいただけでは何もしない)、
そのTranslateTransformのXの値を5秒間でWidthから0に減らすStoryboardを
Resourceに入れておきます。



  private void Grid_Loaded(object sender, RoutedEventArgs e) {
   var container = sender as Grid;
   var vm = container.Tag as TodoViewModel;//ViewModelを渡す簡単な方法が思いつかないので、Tagに入れている
   var id = vm.Id;
   var animation = container.FindResource("_slideInAnimation") as Storyboard;

   if (!_alreadyAnimated.Contains(id)) {
    animation.Begin();
    _alreadyAnimated.Add(id);
   }
  }

んで、Loadedイベントでアニメーションを開始します。
Loadされた時には2番目以降のアイテムが1行ずつ下がっていますので、
新しいアイテムが本来の位置からXだけ右に表示される(そしてXは徐々に減っていく)というわけです。
ただし、それだけだとすべてのアイテムがLoadされるたびにアニメーションされるので、
新規ではないやつはアニメーションしないようにIDを管理しておきます。
これだけです。
Loadedでやるのがスマートではないですね。

次に完了したらフェードアウトする処理です。


        <Storyboard x:Key="_doneAnimation">
         <DoubleAnimation Storyboard.TargetName="_itemContainer"
              Storyboard.TargetProperty="Opacity"
              To="0" BeginTime="0:0:0.5" Duration="0:0:1"/>
         <DoubleAnimation Storyboard.TargetName="_itemBackground"
              Storyboard.TargetProperty="Opacity"
              From="1" Duration="0:0:0.1"/>

背景(WhiteだけどOpacity=0.01→ほぼ透明)を一瞬だけ白くする(Opacity=1)アニメーションと
アイテム自体を徐々に透明にするアニメーションを入れています。

これもMVVM的にVMのコマンドを直接バインドすると
アニメーションをするまえにListBoxから無くなってしまうので、
Viewのコードビハインドのイベントハンドラで


   var animation = container.FindResource("_doneAnimation") as Storyboard;
   animation.Completed += (s, eArg) => {
    vm.CompleteCommand.Execute(null);
   };
   animation.Begin();

というように、StoryboardのCompletedイベントで
ViewModelの完了コマンドを呼ぶようにしています。
(アニメーションしている間はViewModel的には完了していないということです。)

2010年5月10日月曜日

お知らせ

近況ですが、なんか今の作りに不満がでてきたので、作り直しを考えています
不満なのは

  • UnitTestがやりづらい構造になっている
  • twitterにべっとりの作りになっている
  • パフォーマンスが悪い
あとは、単純に.net framework 4.0の機能をがんがん使ってみたかったり。

もちろん今の構造を徐々に修正していくのもありですが、
今ならまだ作り直した方がいいかなぁ、、と。
もちろん流用できるところはそのまま持ってくるので、
作り直しでも、そこまで今までのが無駄になるとは思っていません。

ブログの再開はあと1週間後くらいからかなぁ。。と。

2010年4月19日月曜日

第33回 スキン選択

前回の追記程度ですが。
Appレベルで動的にxamlを読み込むようにしたので、
スキン変更機能を追加します。



 public partial class App : Application {

  protected override void OnStartup(StartupEventArgs e) {
   base.OnStartup(e);
   //オプションの読込
   var option = Option.Instance;

   //アプリケーションの実行ファイルのフォルダ
   var baseUri = Directory.GetParent(
    System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;

   //選択されたスキン名(=フォルダ名)
   //まだ機能追加していないのでデフォルト限定
   var skinFolderName = "Default";
   if (System.IO.Directory.Exists(string.Format(@"{0}\Skins\{1}", baseUri, option.Skin))) {
    skinFolderName = option.Skin;
   }

   //スキンの読込
   ResourceDictionary rd = new ResourceDictionary();
   foreach (DictionaryEntry entry in this.Resources) {//既存のリソースに追加するため
    rd[entry.Key] = entry.Value;
   }

   rd.MergedDictionaries.Add(
    Application.LoadComponent(new Uri("/Resources/CommonTemplates.xaml", UriKind.Relative)) as ResourceDictionary);

   //スキンフォルダの共通スキンを読込(Skinsフォルダ直下は共通)
   foreach (var file in Directory.GetFiles(string.Format(@"{0}\Skins", baseUri), @"*.xaml")) {
    var theme = XamlReader.Load(XmlReader.Create(file)) as ResourceDictionary;
    rd.MergedDictionaries.Add(theme);
   }

   //選択されたスキンの読込
   foreach (var file in Directory.GetFiles(
    string.Format(@"{0}\Skins\{1}", baseUri, skinFolderName),
     @"*.xaml")) {
    var theme = XamlReader.Load(XmlReader.Create(file)) as ResourceDictionary;
    rd.MergedDictionaries.Add(theme);
   }

   this.Resources = rd;

アプリケーションのスタートアップで、
Skinsフォルダ直下と指定したスキンフォルダのxamlのみを読み込みます。
Optionには当然Skinプロパティを追加しています。
(View、ViewModel、Modelすべてに追加していますが、
 すごく簡単なので説明しなくてもわかると思います。)

Skinの増やし方はDefaultフォルダをコピーして、
変更したい部分をちょこちょこと書き換えるだけです。

ただし、しばらくは表示関係は頻繁に修正が入りそうなので、
スキンのカスタマイズの実用はもう少し先になりそうです。

第32回 Windowを透過にする

今回はデザインを大幅に変えてみたいと思います。
こんなかんじ。(アイコンは一応?ぼかしています)
背景が銀色ではなくて、背景は透過でデスクトップの壁紙が見えています。

変更箇所が多すぎて全部網羅して書けないので、要所だけでも。

■Windowを透明にする
Windowのプロパティを、
WindowStyleをNoneにして
AllowsTransparencyをTrueにした上で、
BackgroundをTransparentにします。
直接指定してもいいですが、あとでノーマルな表示との切り替えができる作りにしやすいように、
Styleで指定しておきます。
(Templateはあとで説明します)


 <Style x:Key="WindowStyle" TargetType="{x:Type Window}">
  <Setter Property="Template" Value="{DynamicResource WindowTemplate}"/>
  <Setter Property="WindowStyle" Value="None"/>
  <Setter Property="Background" Value="Transparent"/>
  <Setter Property="AllowsTransparency" Value="True"/>
 </Style>


詳しくはMSDNのWindowのあたりでも。

■タイトルバーを作る
WindowStyleをNoneにしたらタイトルバーまでなくなってしまいます。
ですが、AllowsTransparencyをTrueにしたらタイトルバーを残す方法は無いようです。(たぶん)
というわけで、自力で作ります。



<UserControl x:Class="WTwitter.View.Parts.TitlebarView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:root="clr-namespace:WTwitter"
 xmlns:util="clr-namespace:WTwitter.ViewModel.Utility">
 <DockPanel Background="{DynamicResource TitlebarBackground}">
  <!--左端のアプリのアイコン-->
  <Image Source="{DynamicResource AppImage}" Width="16" Height="16" DockPanel.Dock="Left"/>
  <!--閉じるボタン-->
  <Button DockPanel.Dock="Right" Command="{Binding Path=CloseCommand}" Foreground="White"
    Background="{DynamicResource CloseButtonBackground}"
    Margin="5,0,0,0" Width="25" Height="20" HorizontalContentAlignment="Center" VerticalContentAlignment="Center">
   X</Button>
  <!--オプション画面を開くボタン-->
  <Button Command="{Binding Path=OptionDialogCommand}" DockPanel.Dock="Right"
    Style="{DynamicResource ImageButtonStyle}" Margin="3,1">
   <Image Source="{DynamicResource SettingImage}" Height="16" Width="16"/>
   <Button.ToolTip>設定</Button.ToolTip>
  </Button>
  <!--TwitterのHOMEをブラウザで開くボタン-->
  <Button Command="{x:Static util:CommonCommands.OpenByBrowser}" CommandParameter="http://twitter.com"
    DockPanel.Dock="Right" Style="{DynamicResource ImageButtonStyle}" Margin="3,1">
   <Image Source="{DynamicResource TwitterHomeImage}" Height="16" Width="16"/>
   <Button.ToolTip>TwitterHomeをブラウザで開く</Button.ToolTip>
  </Button>
  <!--タイトルバーのタイトルテキスト-->
  <TextBlock Text="{Binding Path=DisplayName}" Foreground="White" Margin="2"/>
  
  <!--右クリックメニュー-->
  <DockPanel.ContextMenu>
   <ContextMenu>
    <MenuItem Header="最前面に表示(_T)" IsCheckable="True"
        IsChecked="{Binding Source={x:Static root:App.Current}, Path=MainWindow.Topmost, Mode=TwoWay}"/>
   </ContextMenu>
  </DockPanel.ContextMenu>
 </DockPanel>
</UserControl>

ボタンいくつか乗っけたのと、DynamicResourceやらを多用しているので長く見えますが、
基本は横長のバーを表示してタイトル文字列と閉じるボタンをおきたいだけなので、構造は簡単です。



最後にTemplateで作ったタイトルバーとStyleを組み合わせています。
(上のほうで紹介したStyleで指定しています)


 <ControlTemplate x:Key="WindowTemplate" TargetType="{x:Type Window}">
  <DockPanel Background="Transparent">
   <parts:TitlebarView DockPanel.Dock="Top"/>
   <ContentControl Content="{TemplateBinding Property=Content}" Background="Transparent"/>
  </DockPanel>
 </ControlTemplate>



■タイトルバードラッグでのWindowの移動
Windowの枠を無くしちゃったので、
そのままだとWindowの位置を変更する手段がなくなります。
WindowのMouseLeftButtonイベントでDragMove()を呼び出すだけです。
正確にはWindowに書いちゃったので、タイトルバーだけではなく余白領域でも移動します。
(ただ、今の私の実装は余白は透明にしているのでマウスクリックを検知しません。)



  private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
   DragMove();
  }



■Windowのサイズの変更
同様に、枠がなくなるのでそのままではWindowのサイズを変更できなくなります。
これはWindowの上にResizeGripを置くだけで勝手にやってくれるようです。
Gridの上に置いているので、他のコンテンツの上に重ねることができます。
そしてAlignmentでGridの右下に来るようにしています。


  <ResizeGrip HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="3" Height="3" />

あとはMainWindowにResizeMode="CanResizeWithGrip"を設定しておいてください。


■Controlの背景とか
Windowは透明にできたので、あとは上に乗せるものも必要に応じて透明にしていきます。
簡単にあとから変更や調整ができるように、Colors.xamlというファイルに
色の定義をまとめておきます。
以下サンプル。


 <SolidColorBrush x:Key="HitVisibleTransparentBrush">
  <SolidColorBrush.Color>White</SolidColorBrush.Color>
  <SolidColorBrush.Opacity>0.01</SolidColorBrush.Opacity>
 </SolidColorBrush>

 <SolidColorBrush x:Key="MouseOverBackgroundBrush">
  <SolidColorBrush.Color>LightBlue</SolidColorBrush.Color>
  <SolidColorBrush.Opacity>0.5</SolidColorBrush.Opacity>
 </SolidColorBrush>

Opacityで透明度を指定します。

■xamlファイルの配置
プロジェクトに追加されたxamlファイルは
デフォルトではコンパイルされてexeファイルの中にデータが埋め込まれます。
プロパティ(Visual Studio上ソリューションエクスプローラでxamlファイルを右クリック)で
ビルドアクション→コンテンツ、出力ディレクトリ→コピーするを選ぶと
コンパイルされずそのままテキストファイルでbinフォルダのしたに出力されます。
これでxamlファイルをユーザーが修正しても起動時に読み込んで反映されます。
ただし実行時にxamlを解析するためパフォーマンスは落ちます。
あとはApp.xaml.csとかで実行時に読み込みます。

Visual Studioのプロジェクトに取り込んでいないファイルに対しては
動的な読込自体はこれまでも使っていましたが、
Visual Studioに取り込むことにより、DebugビルドとReleaseビルド用に
2回コピーしなくてすみます。(これまで知りませんでした。すみません。)

ここまでのソース
http://wtwitter.codeplex.com/SourceControl/changeset/changes/45389

書いてないことでわからないことがあれば遠慮無く質問を。

注:
Styleとかはファイルの分け方などが安定していないので、
2~3回あとのソースコードを待った方が整理されているかもしれません。

2010年4月14日水曜日

メモ 【修正あり】3タイプのTriggerのサンプル

WPFで凝った表示をしようとすると、Triggerを使うことがよくありますが、
使える組み合わせが限られたり、私はよくこんがらがります。
そこでよく使う3パターンを自分用にメモしておきます。
※もしかしたらもっと単純なやり方があるかもしれません

■パターン1
ある表示要素のプロパティが変わったときに、その要素自体の他のプロパティを変える
例:Borderの上にマウスがのったら、Border自体の色を変える

■パターン2
ある表示要素のプロパティが変わったときに、その要素の中身の他の要素のプロパティを変える
例:Borderの上にマウスがのったら、内側に非表示にしておいたボタンを表示する

■パターン3
表示要素と全く関係ないデータが変わったときに、表示要素のプロパティを変える
例:内部でエラーがおきたら、背景の色を変える

まずパターン3のためのデータのクラスを書きます。
値の変更を検出するためにINotifyPropertyChangedインターフェイスを実装します


 class Model : INotifyPropertyChanged {
  private bool _isOn;
  public bool IsOn {
   get {
    return _isOn;
   }
   set {
    if (_isOn != value) {
     _isOn = value;
     var handler = PropertyChanged;
     if (handler != null) {
      handler(this, new PropertyChangedEventArgs("IsOn"));
     }
    }
   }
  }

  public event PropertyChangedEventHandler PropertyChanged;
 }



以下に3パターンを含んだXamlを示します。


<Window x:Class="TriggerSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:TriggerSample"
        Title="MainWindow" Height="350" Width="525">
 <!--3タイプのTriggerを使ってみたサンプル-->
 
 <Window.DataContext>
  <local:Model x:Name="data"/>
 </Window.DataContext>

 <StackPanel>
  <!--自分自身のプロパティに従って、他のプロパティを変更する-->
  <Border BorderBrush="Green" BorderThickness="2" Margin="5">
   <TextBlock>Borderの上にマウスを置いたらBorderの色が変わる</TextBlock>
   <Border.Style>
    <Style TargetType="{x:Type Border}">
     <Setter Property="Background" Value="Yellow"/>
     <Style.Triggers>
      <Trigger Property="IsMouseOver" Value="True">
       <Setter Property="Background" Value="Red"/>
      </Trigger>
     </Style.Triggers>
    </Style>
   </Border.Style>
  </Border>

  <!--外のプロパティに従って内側のプロパティを変更する-->
  <Border Background="LightBlue" BorderBrush="Red" BorderThickness="3" Name="_outer">
   <StackPanel Orientation="Horizontal">
    <TextBlock>外側のBorderにマウスを置いたら内側のBorderの色が変わる</TextBlock>
    <Border BorderBrush="Black" BorderThickness="3" CornerRadius="3">
     <TextBlock>内側のBorder</TextBlock>
     <Border.Style>
      <Style TargetType="{x:Type Border}">
       <Style.Triggers>
        <DataTrigger Binding="{Binding ElementName=_outer, Path=IsMouseOver}" Value="True">
         <Setter Property="Background" Value="Blue"/>
        </DataTrigger>
       </Style.Triggers>
      </Style>
     </Border.Style>
    </Border>
   </StackPanel>
  </Border>
  
  <!--(Borderに関係ない)データの値に従ってプロパティを変更する-->
  <Border BorderBrush="Pink" BorderThickness="3" Margin="5" Name="border">
   <TextBlock>データの値でBorderの色が変わる</TextBlock>
   <Border.Style>
    <Style TargetType="{x:Type Border}">
     <Setter Property="Background" Value="Orange"/>
     <Style.Triggers>
      <DataTrigger Binding="{Binding ElementName=data, Path=IsOn}" Value="True">
       <Setter Property="Background" Value="DarkGray"/>
      </DataTrigger>
     </Style.Triggers>
    </Style>
   </Border.Style>
  </Border>
  
  <CheckBox IsChecked="{Binding ElementName=data, Path=IsOn, Mode=TwoWay}">ここを押してデータを変える</CheckBox>

 </StackPanel>
</Window>




結果画像

2010年4月12日月曜日

メモ 用語(テンプレート関係)

WPFの仕様を調べたり本を読んだりする上で、
用語が難しいのが一番の障害だと思います。
(私が順にしっかり覚えていっていないだけかもしれませんが。。)

というわけで、自分用の用語集を自分なりの言葉で書いています。
正確ではなかったり、間違っていたり、
他の人にとっては余計にわからなくなるかもしれませんが、あしからず。

データ
ユーザーが入力したりWebから取得したりした情報そのもの

Control
ButtonやListBoxなど。
データを表示したり選択したり、表示を変化させたり、など、『コントロール』するクラス。
多くの場合、.net frameworkで用意されているものを使うが、
カスタムコントロールもある。
※Controlというクラス自体もあるが、ここで説明しているのは
 「なんとかControl」や「Controlなんとか」(ControlTemplateなど)のように使う
 言葉としてのControl

Content
Controlの上に『表示するもの』。
『表示するもの』はデータの場合もあれば、他のControlの場合もある。
ControlにはContentプロパティがありobject型のインスタンスを1つだけ持てる。
複数のContentを持てるControlもあるが、その場合はContentプロパティではなく
ItemsプロパティがContentのリストを表す。

ContentControl
上記で示したような『Contentを1つ取るようなタイプのControl』を示すクラス。
ButtonやListBoxItemなどの具体的なクラスは、このクラスから派生している。

ContentPresenter
Content(=ContentControlクラスが持っているプロパティでobject型)などを、
実行時のデータ型に応じて表示するための仕組みを持つクラス。
Imageだったらそのまま表示したり、テンプレートが指定されていたらそれに従って表示したり、
最悪ToString()した文字列を表示したり、といったことをやってくれる。
具体的にどのように表示方法を決めるのかはエッセンシャルWPFのp.107など参照。
ContentControlはContentPresenterを持ち、
これによってContentプロパティに入っているインスタンスの表示をしている

ContentTemplate
ContentControlの1プロパティ。
Contentの表示用のテンプレートを指定したいときに使う。
ContentTemplateプロパティにはDataTemplateを入れる。

DataTemplate
データはあくまで文字列や数値、またはそれらを含むクラス(主にプログラマが作ったクラス)であり、
UIとしてWindow上にどう描画されるかはわからない。
単純な文字列でさえ、TextBlockという表示要素に格納され、
TextBlockが文字列を表示している。
(特に意識しない場合でもContentPresenterが勝手にやってくれてる)
DetaTemplateは、あるデータがどのようなUI要素(ImageやTextBlock)の組み合わせで
表示されるべきかを決めるもの。
Keyを割り当てておけば、使う場面に応じて、
同じデータを違うテンプレートで表示することができる。

表示ツリー
表示要素は、Windowの上にButtonとTextBlockをおいて、
Buttonの上にはさらにTextBlockがおいてあり、・・・というツリー上になっている。
正確に言えば、

<Button>OK</Button>

とXAMLで書いただけで、
Button→ButtonChrome→ContentPresenter→TextBlock
というツリーができている。このツリーのこと。
Button自体はClickなどのイベントロジックなどボタンの基礎要素であり、
ButtonChromeはButtonの背景イメージであり、
TextBlockはContentPresenterがOKという文字列から判断して生成したもの。
当然OKの部分が文字列ではなかったら、
ContentPresenterはTextBlockじゃないものを生成するかもしれない。

ちなみにこのツリーを作るファクトリがあるが、XAMLで書くときは意識しないでいい。

ControlTemplate
上記のようにControlごとに表示ツリーがある程度決まっているが、
この表示ツリーをカスタマイズするもの。
Button→Ellipse→TextBlock
のように表示ツリーを替えることができる。

Style
Templateがツリー全体をごっそり入れ替えるのに対して、
Styleは一部のプロパティ(たとえばBorderBrushなど)を変更するもの。
Triggerを組み合わせることによって、~したとき色を変える、などができる。

※この文書は公開後も随時更新予定です。

2010年4月4日日曜日

第31回 テストを作る

今更ですがUnitTestを書きます。
ちゃんとした開発ではもっと早くからやるべきことですが、
たぶんblog的にはつまんないテーマなので後回しにしてました。

知っている人は今回全部飛ばしていいような内容ですが、
一応知らない人のために能書きを書いておきます。
知っている部分はチラ読みで飛ばしてください。

UnitTestとは、実際にソフトを動かしてテストするんじゃなくて、
個々のクラスなどの小さな単位用にテスト用のプログラムを書いて、
用意されたテスト用のフレームワークで、(多くの場合は自動で)動かすものです。
UnitTestの用語としてのちゃんとした定義は違うかもしれませんが、
ここではそいうったテストを指すイメージで読んでいってください。
自動化できるので、ソースコードを変更するたびに実行します。

最近よく見るTDD(Test Driven Developmentだったかな?)は
このクラス(メソッド)はこういう呼び出し方したらこういう動作(戻り値など)をするんですよーという
テストを先に書いてから、実際のメソッドの中身のソースコードを書いていきます。

私は他のアプリ使うときにTDDっぽくやってみて、メリットは以下のようなことだと思っています。

・ソースコードを作る側ではなくて、クラスを使う(呼び出す)側の視点で
 クラス/メソッドを設計することになるので、使いやすいクラスができやすい。
・いつでも手軽に実行できるテストコードが残るので、ソースコードを書き換えやすい。
(意図しない副作用が出たら検出しやすいので、安心して書き換えできる)
・第3者にとって、テストコードがクラスの使い方の見本となる
(ある程度大きな規模になるとクラスがどのように動いているのかわかりにくくなるが、
UnitTestのコードはテスト対象とそれを動かすのに最小限のクラスのみが書かれるので、
どうやって使っていいかがわかりやすい)

意外と1個目のメリットが想像以上に大きいかなと思っています。

実際にWTwitterへの適用ですが、
すでにソースコードを結構書いているので、その部分はTDDになりませんが、
これから新しく書くところはできるだけ先にテストコードを書いていきたいと思います。
また既存の部分へのテストは、いっぺんに書くのはやる気が起きないので、
徐々に書いていきます。

あと、カバレッジ(ソースコードのどれだけの割合をテストで実行したか)は100%にすべきだとか、
全クラス/メソッドごとに書くべきだとか
いろいろ人によって意見はあると思いますが、
ぶっちゃけ「twitterクライアントなんて落ちたら再起動すればいいやん」、くらいにしか思ってないので、
費用対効果が大きそうなところしか書きません。
人命に関わるとかエラーの損害が大きいとか品質が大事なソフトではまた変わってくると思います。

で、WTwitterではMbUnit(Gallio)を使ってみます。
比較的新しいテストフレームワークで実は私は使ったことがないのですが、
よそで便利だと評判だったのと、新しい物好きなので使ってみたかったからです。
.net用のフレームワークではnUnitが一番メジャーかなと思います。
VisualStudioの上位版だったら付属しているUnitTestのフレームワークを使うのもいいかもしれません。
というわけで、最初のうちはMbUnitの利点を生かせないテストになっているかもしれませんが、
それを念頭に置いて以降を読んでください。

準備は、まずGallioをインストールしてください
http://www.gallio.org/

そしてVisualStudioでテスト用の新しいプロジェクト(クラスライブラリ)を作成。
テストプロジェクトにDLL参照の追加(WTwitter, MbUnit, Gallio)

WTwitter本体の方のプロジェクトに戻ってテスト対象をpublicにしてください。
別プロジェクトになるとpublic じゃないとテストできないので注意してください
 (internalのままでテストプロジェクトから見えるようにする方法はあります。)

テストプロジェクトにテスト用のクラスを作成。
テストメソッドには[Test]の属性を付けます。
※nUnitにはクラスに[TestFixture]の属性が必要だったと思いますが、
MbUnitでは必要なくなったのかな?
なくても動くようです。



namespace WTwitter.Tests.Model.Twitter {
 class TwitterItemTest {
  private static class TestDataFactory  {
   public static  Status CreateStatus() {
    return new Status() {
     Id = 100,
     Favorited = false,
     CreatedAtString = "Mon Jan 11 15:26:14 +0000 2010",
     Text = "おはようございます",
     InReplyToScreenName = "yuki",
     InReplyToStatusId = null,
     InReplyToUserId = null,
     Truncated = false,
     User = new User() {
      Id = 33,
      Following = false,
      Name = "yuki_",
      ScreenName = "yuki",
      Url = "http://wtwitter.codeplex.com/",
      ProfileImageUrl = "http://someimage",
      Description = "description for user",
     }
    };
   }
  }

  [Test]
  public void TestConstructor() {
   var status = TestDataFactory.CreateStatus();
   var target = new TwitterItem(status);
   Assert.AreEqual(100,target.Id);
   Assert.IsFalse(target.Favorited);
   var date = new DateTime(2010, 1, 11, 15, 26, 14, DateTimeKind.Utc);
   Assert.AreEqual(date.ToLocalTime(), target.CreatedAt, "CreatedAtはLocalTimeで持つ");
   Assert.AreEqual(ItemType.TwitterStatus, target.Type);
   Assert.AreEqual("おはようございます", target.Text);
   Assert.AreEqual("yuki", target.User.ScreenName);
   Assert.AreEqual("yuki_", target.User.Name );
   Assert.AreEqual("description for user", target.User.Description );
   
  }

  [Test]
  public void TestEquality() {
   //EqualsはIdで判断する

   var status1 = TestDataFactory.CreateStatus();
   var status2 = TestDataFactory.CreateStatus();
   var status3 = TestDataFactory.CreateStatus();
   status1.Id = 10;
   status2.Id = 10;
   status3.Id = 20;
   var item1 = new TwitterItem(status1);
   var item2 = new TwitterItem(status2);
   var item3 = new TwitterItem(status3);

   Assert.IsTrue(item1.Equals(item2));
   Assert.IsFalse(item1.Equals(item3));
  }

  [Test]
  public void TestTextComponent() {
   //細かい動作はSplitterクラスのテストで確認する

   var status1 = TestDataFactory.CreateStatus();
   var item1 = new TwitterItem(status1);
   Assert.AreEqual(1, item1.TextComponents.Count);

   var status2 = TestDataFactory.CreateStatus();
   status2.Text = "@yuki1090 てすと http://www.google.com てすと";
   var item2 = new TwitterItem(status2);
   
   //※"@" "yuki1090" " てすと " "http://www.google.com" " てすと"に分かれる
   //IDに@は含まれないので注意
   Assert.AreEqual(5, item2.TextComponents.Count);
   Assert.AreEqual(TextComponentType.Plain, item2.TextComponents[0].Type);
   Assert.AreEqual(TextComponentType.UserName, item2.TextComponents[1].Type);
   Assert.AreEqual(TextComponentType.Plain, item2.TextComponents[2].Type);
   Assert.AreEqual(TextComponentType.Url, item2.TextComponents[3].Type);
   Assert.AreEqual(TextComponentType.Plain, item2.TextComponents[4].Type);
   Assert.AreEqual("@", item2.TextComponents[0].Text);
   Assert.AreEqual("yuki1090", item2.TextComponents[1].Text);
   Assert.AreEqual(" てすと ", item2.TextComponents[2].Text);
   Assert.AreEqual("http://www.google.com", item2.TextComponents[3].Text);
   Assert.AreEqual(" てすと", item2.TextComponents[4].Text);
  }
 }
}



このテスト対象クラスはStatusクラスのデータをそのまま渡すだけのプロパティが多いので、
コンストラクタのテストと、Equalsのロジックとテキスト分解のロジックをテストしているだけです。
テキスト分解は実際には別クラスがやっているので、
そちらでいろんなパターンのテキストをテストする予定です。
(このクラスのテストではちゃんと呼び出しているかどうかのチェックくらいです)

今回のようにすでにコードを書いている状態であとからテストする場合は、
メソッド名やその上にXmlで書いているコメントのみからテストを書けるかどうか試すといいかと思います。
ソースコードのアルゴリズムを読まないとテストを書けない(使い方や出力がわからない)ような部分は
名称を工夫するなりコメントを増やすなりリファクタリングするなりした方がいいかもしれません。

テストの実行はスタートメニューからIcarus GUI Test Runnerを実行。
テストプロジェクトをビルドして
Icarus(Gallio)に出力されたdllを読み込んで、Start。
全部緑になればOK。

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

2010年4月1日木曜日

お知らせ

これまでソースコードのみの公開でしたが、
β 0.01をバイナリで公開しました。

http://wtwitter.codeplex.com/

まっさらな状態で起動して起こる不具合をいくつか直しています。
大きな機能追加はないです。
主にオプション周りです。
一番大きいのは、
ID認証、タイムラインの追加、削除に関して再起動無しで反映されるようにしました。

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