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認証、タイムラインの追加、削除に関して再起動無しで反映されるようにしました。