かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WCF RIA Services + Silverlight + NPOIでエクセルダウンロード処理を作ろう

タイトルの通りです。因みにMSDNフォーラムの質問が元ネタです。

前にNPOIを使った記事を書いたりしましたが、それを使ってサーバーサイドでExcelのバイナリを作って転送してSilverlight側で保存すればいいんじゃないんだろうかというロジックです。

早速作ってみよう

まず、ExcelDownloadSampleという名前でSilverlightアプリケーションを作成します。もちろんWCF RIA Servicesは有効にしておきます。そしてDomainServiceを作成します。名前はExcelServiceにしておきました。

とりあえず、エンテティとしてPersonクラスを作成します。

/// <summary>
/// Entity
/// </summary>
public class Person
{
    [Key]
    [Display(AutoGenerateField = false)]
    public int ID { get; set; }

    [Display(Name = "名前")]
    public string Name { get; set; }

    [Display(Name = "誕生日")]
    public DateTime Birthday { get; set; }
}

そして、適当にテストデータを作って、それを取得するためのメソッドをサービスに作成します。

// sample data
private static readonly IEnumerable<Person> db = Enumerable.Range(1, 100)
    .Select(i =>
        new Person
        {
            ID = i,
            Name = "田中 太郎" + i,
            Birthday = DateTime.Now
        });

// get sample data
public IQueryable<Person> GetPeople()
{
    return db.AsQueryable();
}

そして、画面をデザインします。適当に上にLoadボタンとExport...ボタンがあって残りはDataGridが置いてあるシンプルな画面にしました。

<UserControl x:Class="ExcelDownloadSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="254*" />
        </Grid.RowDefinitions>
        <StackPanel Name="stackPanel1" Orientation="Horizontal">
            <Button Content="Load" Name="buttonLoad" MinWidth="75" Margin="2.5" Click="buttonLoad_Click" />
            <Button Content="Export..." Name="buttonExport" MinWidth="75" Margin="2.5" Click="buttonExport_Click" />
        </StackPanel>
        <sdk:DataGrid Grid.Row="1" Name="dataGridPeople" IsReadOnly="True" AutoGenerateColumns="False">
            <sdk:DataGrid.Columns>
                <sdk:DataGridTextColumn Binding="{Binding ID}" />
                <sdk:DataGridTextColumn Binding="{Binding Name}" />
                <sdk:DataGridTextColumn Binding="{Binding Birthday, StringFormat=yyyy年MM月dd日}" />
            </sdk:DataGrid.Columns>
        </sdk:DataGrid>
    </Grid>
</UserControl>

デザイナ上ではこんな感じです。

そして、コードビハインドでデータを取得してDataGridに表示します。今回はダウンロードが主目的なのでMVVMではなく、コードビハインドにべったり書きます。

public partial class MainPage : UserControl
{
    private ExcelContext context = new ExcelContext();

    public MainPage()
    {
        InitializeComponent();
    }

    private void buttonLoad_Click(object sender, RoutedEventArgs e)
    {
        // データを取得してきてDataGridに表示する
        context.Load(
            context.GetPeopleQuery(),
            op =>
            {
                this.dataGridPeople.ItemsSource = op.Entities;
            },
            null);
    }
}

これで実行してLoadボタンを押すとデータがDataGridに表示されます。

いよいよ本題のExcelダウンロード

では、早速Excelのダウンロードの処理を書いていきます。全体をさくっと貼ります。クラス分けするのもめんどくさかったしサンプルなんでExcelServiceクラスの中にべったりと書いてます(悪)

// get excel binary
[Invoke]
public byte[] GetExecl()
{
    var book = CreateWorkbook(db);
    var ms = new MemoryStream();
    book.Write(ms);
    return ms.ToArray();
}

private static HSSFWorkbook CreateWorkbook(IEnumerable<Person> source)
{
    var book = new HSSFWorkbook();
    var sheet = book.CreateSheet("Person");

    CreateHeaderRow(book, sheet);
    foreach (var p in source.Select((person, index) => new { person, index }))
    {
        CreateRow(book, sheet, p.index + 1, p.person);
    }
    return book;
}

// ヘッダー行を作成する
private static void CreateHeaderRow(HSSFWorkbook book, Sheet sheet)
{
    var row = sheet.CreateRow(0);

    // 0列目はIDの列
    var idCell = row.CreateCell(0);
    idCell.SetCellValue("ID");

    // 1列目は名前の列
    var nameCell = row.CreateCell(1);
    nameCell.SetCellValue("名前");

    // 3列目は誕生日の列
    var birthdayCell = row.CreateCell(2);
    birthdayCell.SetCellValue("誕生日");

    // 4方に罫線
    var style = book.CreateCellStyle();
    style.BorderTop = CellBorderType.THIN;
    style.BorderLeft = CellBorderType.THIN;
    style.BorderBottom = CellBorderType.THIN;
    style.BorderRight = CellBorderType.THIN;

    // 薄いグリーンの背景色で塗りつぶす
    style.FillForegroundColor = IndexedColors.LIGHT_GREEN.Index;
    style.FillPattern = FillPatternType.SOLID_FOREGROUND;
    // テキストはセンタリング
    style.Alignment = HorizontalAlignment.CENTER;

    // 太字
    var font = book.CreateFont();
    font.Boldweight = (short)FontBoldWeight.BOLD;
    style.SetFont(font);

    // 全てのヘッダー用のセルに、上で作ったスタイルを適用する
    foreach (var cell in new[] { idCell, nameCell, birthdayCell })
    {
        cell.CellStyle = style;
    }

}

// index行目のデータを作る
private static void CreateRow(HSSFWorkbook book, Sheet sheet, int index, Person p)
{
    // 行を作って
    var row = sheet.CreateRow(index);

    // id列を作る
    var idCell = row.CreateCell(0);
    idCell.SetCellValue(p.ID);

    // 名前も適当に入れて
    var nameCell = row.CreateCell(1);
    nameCell.SetCellValue(p.Name);

    // 誕生日も適当に
    var birthdayCell = row.CreateCell(2);
    birthdayCell.SetCellValue(p.Birthday);

    // 全ての列に4方に罫線のあるスタイルを作って適用する
    // あえて別々のスタイルを設定してるのは、誕生日セルにフォーマットを入れるため
    foreach (var cell in new[] { idCell, nameCell, birthdayCell })
    {
        var style = book.CreateCellStyle();
        style.BorderTop = CellBorderType.THIN;
        style.BorderRight = CellBorderType.THIN;
        style.BorderLeft = CellBorderType.THIN;
        style.BorderBottom = CellBorderType.THIN;

        cell.CellStyle = style;
    }
    // 日付用yyyy年mm月dd日のフォーマットで誕生日は表示するようにする
    var format = book.CreateDataFormat();
    birthdayCell.CellStyle.DataFormat = format.GetFormat("yyyy年mm月dd日");
}

ほぼ、前に書いた記事のコードの使いまわしですが、細かいところでAPIに変更が入ってたりするので、このコードが今の所最新バージョンで動く感じです。Personのデータを純粋にExcelシートに書き込んで行って、最後にMemoryStreamに吐き出してbyte[]として返しています。Silverlight側の処理は以下のようになります。

private void buttonExport_Click(object sender, RoutedEventArgs e)
{
    // Excelファイルを保存する
    var dlg = new SaveFileDialog();
    dlg.Filter = "Excelファイル(*.xls)|*.xls|全てのファイル|*.*";

    // Dialogはユーザからの操作で開かせる必要があるのでここで開かせる
    if (dlg.ShowDialog() != true)
    {
        return;
    }

    context.GetExecl(
        op =>
        {
            // データの取得が終わったらファイルに書き込む
            using (var f = dlg.OpenFile())
            {
                f.Write(op.Value, 0, op.Value.Length);
            }
            MessageBox.Show("保存終了");
        },
        null);
}

ポイントは、コメントにも書いてありますがボタンクリックのところで必ずファイルを開くダイアログを出すところです。じゃないとダイアログ開かせてくれません。
保存するファイルが選択されたら、サーバーからデータを取得して最後にファイルにデータを吐き出しています。


実行すると以下のような感じです。

保存されたExcelを開いてみるとちゃんと出てます。

コードのダウンロード

このサンプルプログラムの完全なコードは以下からダウンロードできます。