僕としてはマルチ TIFF の変換が目的でしたが、このコードで JPEG やら PNG やらも変換できます。
Window1.xaml.cs
using System;
using System.IO;
using System.IO.Packaging;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media.Imaging;
using System.Windows.Markup;
using System.Windows.Xps.Packaging;
using System.Windows.Xps;
using Microsoft.Win32;
namespace WpfApplication1
{
public partial class Window1 : Window
{
private XpsDocument _xpsDocument;
private readonly Uri _packageUri;
private readonly OpenFileDialog _openImageFileDialog;
public Window1()
{
InitializeComponent();
this._xpsDocument = null;
this._packageUri = new Uri("pack://work.xps");
this._openImageFileDialog = new OpenFileDialog();
this._openImageFileDialog.Filter = "Image Files|*.bmp;*.jpg;*.jpeg;*.png;*.gif;*.tif;*.tiff";
}
private void _openButton_Click(object sender, RoutedEventArgs e)
{
if (!this._openImageFileDialog.ShowDialog(this).Value)
{
return;
}
if (this._xpsDocument != null)
{
this.CloseDocument();
}
using (Stream imageStream = this._openImageFileDialog.OpenFile())
{
this.BuildXpsDocument(imageStream);
this._documentViewer.Document = this._xpsDocument.GetFixedDocumentSequence();
}
}
private void BuildXpsDocument(Stream imageStream)
{
/*** FixedDocument の作成 ***/
FixedDocument imagesDocument = new FixedDocument();
BitmapDecoder bitmapDecoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.None, BitmapCacheOption.Default);
foreach (BitmapFrame sourceFrame in bitmapDecoder.Frames)
{
// sourceFrame を直接使用してしまうと、先頭フレーム以外リソース化されない
BitmapFrame separatedFrame = BitmapFrame.Create(sourceFrame);
Size frameSize = new Size(separatedFrame.PixelWidth, separatedFrame.PixelHeight);
PageContent imagePageContent = new PageContent()
{
Width = frameSize.Width,
Height = frameSize.Height
};
FixedPage imagePage = new FixedPage()
{
Width = frameSize.Width,
Height = frameSize.Height
};
Image image = new Image()
{
Width = frameSize.Width,
Height = frameSize.Height,
Source = separatedFrame
};
FixedPage.SetTop(image, 0);
FixedPage.SetLeft(image, 0);
imagePage.Children.Add(image);
((IAddChild)imagePageContent).AddChild(imagePage);
imagesDocument.Pages.Add(imagePageContent);
}
/*** XpsDocument の作成 ***/
MemoryStream packageStream = new MemoryStream();
Package package = Package.Open(packageStream, FileMode.Create, FileAccess.ReadWrite);
// メモリ上の XPS を DocumentViewer に表示するためには Package に URI を割り当てる必要がある
PackageStore.AddPackage(this._packageUri, package);
this._xpsDocument = new XpsDocument(package, CompressionOption.NotCompressed, this._packageUri.AbsoluteUri);
/*** XpsDocument に FixedDocument を書き込む ***/
XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(this._xpsDocument);
writer.Write(imagesDocument);
}
private void CloseDocument()
{
this._documentViewer.Document = null;
this._xpsDocument.Close();
Package package = PackageStore.GetPackage(this._packageUri);
package.Close();
PackageStore.RemovePackage(this._packageUri);
}
}
}
Window1.xaml
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="419" Width="648">
<Grid>
<ToolBar Band="1" BandIndex="1" Height="26" Name="_mainToolBar" VerticalAlignment="Top" >
<Button Name="_openButton" Height="20" Width="40" Margin="0,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" Click="_openButton_Click">開く</Button>
</ToolBar>
<DocumentViewer Name="_documentViewer" VerticalAlignment="Top" Margin="0,26,0,0" />
</Grid>
</Window>
動作させるには、Visual Studio にて "WpfApplication1" という名前の WPF アプリケーションプロジェクトを作成して、各コードファイルを上書きしてください。
また、ReachFramework.dll と System.Printing.dll への参照設定が必要となります。
static class Program
{
static void Main()
{
Hoge h = null;
h.Safely().Fuga();
}
}
public class Hoge
{
static public readonly Hoge Null = new NullHoge();
public virtual void Fuga()
{
}
private class NullHoge : Hoge
{
}
}
static public class HogeExtension
{
static public Hoge Safely(this Hoge source)
{
return (source != null) ? source : Hoge.Null;
}
}
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim h As Hoge = Nothing
h.Safely()
h.Fuga()
End Sub
End Module
Public Class Hoge
Public Shared ReadOnly Null As Hoge = New NullHoge()
Public Sub Fuga()
End Sub
Private Class NullHoge
Inherits Hoge
End Class
End Class
Public Module HogeExtension
<Extension()> _
Public Sub Safely(ByRef source As Hoge)
If (source Is Nothing) Then
source = Hoge.Null
End If
End Sub
End Module
方法は簡単で、HogeOperation<TResult> クラスが行っていた戻り値をフィールドに保持する処理を、各処理クラスが直接行えば良いだけ。
GetFugasOperation クラス
internal sealed class GetFugasOperation : HogeOperation
{
public GetFugasOperation(HogeService service)
: base(service)
{
}
private IList<Fuga> _result;
public IList<Fuga> Execute()
{
this.Processing();
return this._result;
}
protected override void MainProcessing()
{
this._result = this._service.GetFugas();
}
}
GetFugaOperation クラス
internal sealed class GetFugaOperation : HogeOperation
{
public GetFugaOperation(HogeService service)
: base(service)
{
}
private int _id;
private Fuga _result;
public Fuga Execute(int id)
{
this._id = id;
this.Processing();
return this._result;
}
protected override void MainProcessing()
{
var findFugaById =
from fuga in this._service.GetFugas()
where (fuga.Id == this._id)
select fuga;
this._result = findFugaById.Single();
}
}
どうせ引数は各処理クラスでフィールド化しなきゃいけないのだから、戻り値も各処理クラスでフィールド化した方が自然な気がする。
とても似ているメソッドの共通化
ロギングのような別の関心事に関する共通処理ならばアスペクト指向という手もある。
が、ここではそういうケースではなく、あくまでも同一の関心事であるケースについて考えてみようと思う。
同一の関心事であるケースとは、コアロジックに関係する事前処理・事後処理のことである。
基本的には、リンク先に書かれている方法で充分だ。しかし、各処理が複雑な場合、一つのクラスに大量のプライベートメソッドが出てくる。
これは共通の事前処理・事後処理があるかどうかは無関係な話だが、そうすると各処理をクラス化した方がよくなるわけだ。
各処理クラスは、共通の基本クラスから派生する。
そして事前処理・事後処理は基本クラスで実装し、テンプレートメソッドパターンを適用することで、派生クラスにはコアロジックの実装に専念させる。
引数、戻り値、例外の扱い方に色々な方法があるが、僕がお勧めする方法を例を交えて 1 つだけ紹介しようと思う。
まず、次のような 2 つのクラスがあるとする。(メソッドの中身は省略)
HogeService クラス
public class HogeService
{
public void Open();
public void Close();
public void AddFuga(Fuga target);
public IList<Fuga> GetFugas();
}
Fuga クラス
public class Fuga
{
public int Id { get; }
}
HogeService クラスの AddFuga(Fuga target) や GetFugas() を使用する場合、次の制約がある。
- 事前処理として、Open を呼び出さなければならない
- 事後処理として、Close を呼び出さなければならない
Hoge クラスでは、Open・Close を隠蔽し、また ID を元に単一の Fuga を取得するメソッドを新たに提供することにした。
Hoge クラス
public class Hoge
{
public void AddFuga(Fuga target)
{
HogeService service = new HogeService();
service.Open();
try
{
service.AddFuga(target);
}
finally
{
service.Close();
}
}
public IList<Fuga> GetFugas()
{
HogeService service = new HogeService();
service.Open();
try
{
return service.GetFugas();
}
finally
{
service.Close();
}
}
public Fuga GetFuga(int id)
{
HogeService service = new HogeService();
service.Open();
try
{
var findFugaById =
from fuga in service.GetFugas()
where (fuga.Id == id)
select fuga;
return findFugaById.Single();
}
finally
{
service.Close();
}
}
}
見ての通り、各処理は try ブロック内が異なるだけで他は全て同一の処理を行っている。
ではここで、各処理をクラス化してみよう。
まず、基本クラスである HogeOperation クラス。
HogeOperation クラス
internal abstract class HogeOperation
{
public HogeOperation(HogeService service)
{
this._service = service;
}
protected readonly HogeService _service;
protected void Processing()
{
this._service.Open();
try
{
this.MainProcessing();
}
finally
{
this._service.Close();
}
}
protected abstract void MainProcessing();
}
戻り値を持たない処理は、この HogeOperation を直接継承する。
また、外部から処理を実行できるよう、適切なパラメータを持つ Execute メソッドを用意する。受け取った引数は、MainProcessing メソッドで使用できるよう、フィールドに保持しておく。
AddFugaOperation クラス
internal sealed class AddFugaOperation : HogeOperation
{
public AddFugaOperation(HogeService service)
: base(service)
{
}
private Fuga _target;
public void Execute(Fuga target)
{
this._target = target;
return this.Processing();
}
protected override void MainProcessing()
{
this._service.AddFuga(this._target);
}
}
続いて、戻り値を持つ処理の基本クラスとなる、HogeOperation<TResult> クラス。
このクラスは HogeOperation クラスを継承するのだが、new 修飾子を使用して、Processing メソッドが戻り値を返すように再定義を行っている。MainProcessing メソッドも戻り値を返せるようにする必要があるが、抽象メソッドなので new 修飾子による再定義はできない。代わりに、out パラメータとして戻り値を返せるバージョンの MainProcessing メソッドを新たに定義している。
HogeOperation<TResult> クラス
internal abstract class HogeOperation<TResult> : HogeOperation
{
public HogeOperation(HogeService service)
: base(service)
{
}
private TResult _result;
protected new TResult Processing()
{
base.Processing();
return this._result;
}
protected override void MainProcessing()
{
this.MainProcessing(out this._result);
}
protected abstract void MainProcessing(out TResult result);
}
戻り値を持つ処理は、この HogeOperation<TResult> クラスを継承する。
こちらも、外部から処理を実行できるよう、適切なパラメータを持つ Execute メソッドを用意する。
GetFugasOperation クラス
internal sealed class GetFugasOperation : HogeOperation<IList<Fuga>>
{
public GetFugasOperation(HogeService service)
: base(service)
{
}
public IList<Fuga> Execute()
{
return this.Processing();
}
protected override void MainProcessing(out IList<Fuga> result)
{
result = this._service.GetFugas();
}
}
GetFugaOperation クラス
internal sealed class GetFugaOperation : HogeOperation<Fuga>
{
public GetFugaOperation(HogeService service)
: base(service)
{
}
private int _id;
public Fuga Execute(int id)
{
this._id = id;
return this.Processing();
}
protected override void MainProcessing(out Fuga result)
{
var findFugaById =
from fuga in this._service.GetFugas()
where (fuga.Id == this._id)
select fuga;
result = findFugaById.Single();
}
}
これらの処理クラスを使用することで、Hoge クラスは次のようにシンプルなコードとなる。
Hoge クラス
public class Hoge
{
public void AddFuga(Fuga target)
{
HogeService service = new HogeService();
new AddFugaOperaion(service).Execute(target);
}
public IList<Fuga> GetFugas()
{
HogeService service = new HogeService();
return new GetFugasOperaion(service).Execute();
}
public Fuga GetFuga(int id)
{
HogeService service = new HogeService();
return new GetFugaOperaion(service).Execute(id);
}
}
以上で処理のクラス化は完了となる。
// 追記 (2008/09/24)
HogeOperation<TResult> クラスは、ややトリッキーだったかもしれない。別解を記事にしたのでこちらも参照して頂きたい。
戻り値を持つ処理のクラス化の別解
// 追記ここまで
さて、最初にも書いたが、各処理が複雑でない内は、デリゲートの注入で充分だろう。この例では各処理が複雑なものではないため、各処理をクラス化したことによって逆に複雑になってしまっている。だが、処理が複雑化してきたときには、処理をクラス化することは非常に有効である。これは単一責任の原則を適用するということだ。例えば Hoge クラスに何らかの状態を持たせたりする場合、状態操作の処理が Hoge クラス内で行われる。各処理で行われる複雑な処理は、処理クラスに切り離されているため、Hoge クラスが責任を持つべき処理だけが Hoge クラス内に残るわけである。
ところで、HogeOperation クラス、HogeOperation<TResult> クラスには、protected な Processing メソッドを用意して、外部から処理を実行させるための Execute メソッドは実際の処理クラスに用意しているが、なぜわざわざこのようなことをしているのか。
例えば、引数はコンストラクタなり引数設定用のメソッドなりで設定できるようにしてしまえば、Processing メソッドの代わりに public な Execute メソッドを基本クラスに用意できるだろう。
しかし、そうすると各処理がスローする可能性のある例外を明示する時、つまり、XML コメントの再定義で困る。結局 XML コメントを再定義するためには Execute メソッドをオーバーライドするなり new 修飾子で再定義するなりしなければならない。ならば、初めから Execute メソッドは処理クラスで定義しなければならないようにした方が良いと考えたわけだ。
無論、XML コメントの再定義を行わないつもりならば、引数はコンストラクタなり引数設定用のメソッドなりで設定できるようにして、基本クラスで Execute メソッドを定義してしまっても良い。
引数設定用のメソッドを用意するのなら、フルエントインターフェイスを意識すると良い。
当初はこういった別の実装方法も取り上げるつもりだったのだが、収拾がつかなくなってしまったため省略した。
Mokosh | VsCommands
ソリューションエクスプローラで、ファイルを複数選択して右クリック → [Group Items] をクリックすると選択したファイルがグループ化される。多段の入れ子にすることもできる。
ファイルの複数選択は Ctrl キー押しながらファイルをクリックしてけばオッケー。
ちなみに、ファイルを入れ子にするって指定は元々プロジェクトファイルがサポートしている機能 (DependentUpon 要素) なので、VSCommands をインストールしていない Visual Studio で開いてもちゃんと入れ子になって表示される。
よし、これでさっきここに書いた
├─ Hoge.cs
├─ Hoge.Test.cs
├─ HogeFactory.cs
└─ HogeFactory.Test.cs
ってのが
□─ Hoge.cs
└─ Hoge.Test.cs
□─ HogeFactory.cs
└─ HogeFactory.Test.cs
って具合に入れ子にできる (^ω^)
追記 (2008/10/17)
バージョン 1.2 がリリースされています。
嬉しい事に、Ctrl キーを押しながら Group Items をクリックすることで、ルートにしたいファイルを選択可能になっています。
また、ファイルを入れ子にする機能以外の機能がいくつか備わったようです。
それと、リンク先の URL が変更になったようなので貼り直しました。
理由は、同一プロジェクトに格納するとリリース時にそれらを除外しなければならないから。
でも一回そういうスクリプト書いてしまえば使い回しが利くだろうし、これ以外に大した理由はない。
しかし、bleis-tift さんは、僕とは違い同一プロジェクトに含める派で、除外するための手間を逆手に取っている。
C#でのテスト(1) テスト用プロジェクト(?) - 予定は未定Blog版
なるほど、確かにそういう考え方もできる。
また、同一プロジェクトに含む場合、テスト対象とテストの距離が近くなるなど、他にもメリットがあるのだと思う。
所で、最近僕はそもそも同一プロジェクトに含んだとしてもテストコードを除外する必要はないのではないかと疑問に感じている。
テストコードが含まれていることで何かしらの不都合が生じることなんてないのではなかろうか。
強いて言うならアセンブリのファイルサイズが若干大きくなることくらいか、と。
ここで問題となるのが (テストコード除外による) ビルドツールの強制というメリットがなくなるという点。
一見するとデメリットだ。
しかし、テストコードを除外することに本質的な意味がないのであれば、ビルドツールの強制のためだけにテストコード除外というプロセスを組み込むことになってしまう。
つまり、突き詰めればそれはビルドツールを使えと単に指示することと変りないことになる。テストコードを除外する必要がないのだから。
テストコードが含まれていても不都合が本当に生じないのかどうかはわからない。実際に試してみる必要もありそうだ。