[WPF][XAML]VB-ReportのWPF 用ビューアコントロールを使ってみる

# VB-ReportのWPF 用ビューアコントロールを使ってみる

## 開発環境
- Visual Studio 2017 (C#)
- Visual Studioの拡張機能の「Prism Template Pack」
  - Prism.Core 6.3.0
  - Prism.Unity 6.3.0
  - Prism.Wpf 6.3.0
  - CommonServiceLocator 1.3.0
  - Unity 4.0.1
- VB-Report 8.0

## 初めに
WPFアプリケーションでアドバンスソフトウェア社のVB-Reportを使って帳票のプレビュー画面を表示するというのが今回の目的です。

## VB-Reportについて
アドバンスソフトウェア社のVB-Reportは、Microsoft Excelで帳票をデザインする帳票ツールです。
使い慣れたExcelでそのまま帳票設計ができるので、専用のデザイナを使用しなくてもデザインが作れる分かなり効率的に帳票が作成作業が行えます。
「VB」とついていますが、VB.NETだけでなくC#でも使用できます。
さまざまな開発で使用できます。

- WindowsForm用ビューアコントロール
- Web用ビューアコントロール
- WPF 用ビューアコントロール
- ブラウザ用 ActiveX ビューアコントロール
- Windows ストアアプリ用ビューアコントロール

※オープンソースではなく、有料のコンポーネントになっています。

[VB-Report8の製品情報はこちら](http://www.adv.co.jp/product/product_vb-report8.htm)

[体験版のダウンロードはこちら](http://www.adv.co.jp/download/download.htm)

### コードのサンプル
以下は、簡単なサンプルですが非常に楽に帳票出力が可能です。

```vbnet:VB.NET
' ①-2帳票ドキュメントの作成を開始します。
CellReport1.Report.Start()
' ①-3帳票の作成を開始します。
CellReport1.Report.File()
' ②-1デザインシートを指定します。
CellReport1.Page.Start("Sheet1", "1")
' ②-2セルに値を設定します。
CellReport1.Cell("A1").Value = "VB-Reportのサンプル";
' ②-3ページ処理を終了します。
CellReport1.Page.End()
' ③-1帳票ドキュメント作成を終了します。
CellReport1.Report.End()
' ③-2作成した帳票ドキュメントをビューアに設定します。
ViewerControl1.Document = CellReport1.Document
```

```csharp:C#
// ①-1デザインファイル名を指定します。
cellReport1.FileName = @"C:\Book1.xlsx";
// ①-2帳票ドキュメントの作成を開始します。
cellReport1.Report.Start();
// ①-3帳票の作成を開始します。
cellReport1.Report.File();
// ②-1デザインシートを指定します。
cellReport1.Page.Start("Sheet1", "1");
// ②-2セルに値を設定します。
cellReport1.Cell("A1").Value = "VB-Reportのサンプル";
// ②-3ページ処理を終了します。
cellReport1.Page.End();
// ③-1帳票ドキュメント作成を終了します。
cellReport1.Report.End();
// ③-2作成した帳票ドキュメントをビューアに設定します。
viewerControl1.Document = cellReport1.Document;
```

## 説明省略部分
- 「Prism Template Pack」のインストール
- 「Prism Template Pack」のプロジェクト作成
-  「VB-Report」のインストール
-  「Prism + Unity」アプリケーションの作り方

※「Prism Template Pack」のインストールは、以前の記事を参考にしてください。
[ PrismとUnityを使うWPFプロジェクトテンプレートを使ってみる](http://qiita.com/HAGITAKO/items/adedbb6c70d2763543f2)

## WPF用ビューアを配置するとバインディングができない。

```xml:MainUcView.xaml
<UserControl
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             xmlns:vbr="clr-namespace:AdvanceSoftware.VBReport8.WPF;assembly=VBReport8.WPF.Viewer" x:Class="VbReportSampleApp.Views.MainUcView"             
             prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <vbr:ViewerControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Document="{Binding DocumentReport}"/>
    </Grid>
</UserControl>
```

普通に上のコードのようにMVVMでドキュメントをバインディングしようとするとDependencyPropertyがDocumentにないのでエラーが発生します。


![WS000000213.JPG](https://qiita-image-store.s3.amazonaws.com/0/90597/969cfa7c-08d0-a11f-c89c-bb16ec306472.jpeg)

これでは、MVVMのアプリケーションなのにViewModel.csではなく、xaml.csに直接書かなければとなってしまいますが、解決方法はあります。
 DependencyPropertyを作ってしまえばよいのです。
 なんだか難しそうに感じますが、実は簡単なようです。

  1. ViewerControlを使用したコントロールを自作する。
  2. 自作したViewControlで作ったバインディング先を介してドキュメントを渡す。
 
 以下の説明からその手順になります。

## 作成手順

### 「ViewerControlEx.xaml」の作成

- Viewsフォルダを右クリック > 追加 > 新しい項目
- WPF > ユーザーコントロール(WPF)を選択
- コントロール名は、「ViewerControlEx」
- XAMLにViewControlを配置する
 
```xml:ViewerControlEx.xaml
<UserControl x:Class="VbReportSampleApp.Views.ViewerControlEx"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:vbr="clr-namespace:AdvanceSoftware.VBReport8.WPF;assembly=VBReport8.WPF.Viewer"
             xmlns:local="clr-namespace:VbReportSampleApp.Views"
             mc:Ignorable="d" 
             d:DesignHeight="600" d:DesignWidth="600">
    <vbr:ViewerControl x:Name="viewer" />
</UserControl>
```


### 「ViewerControlEx.xaml.cs」の作成
  
 - xaml.csにDocumentPropertyを作成する

```csharp:ViewerControlEx.xaml.cs
using AdvanceSoftware.VBReport8;
using System.Windows;
using System.Windows.Controls;

namespace VbReportSampleApp.Views
{
    /// <summary>
    /// ViewerControlEx.xaml の相互作用ロジック
    /// </summary>
    public partial class ViewerControlEx : UserControl
    {
        public ViewerControlEx()
        {
            InitializeComponent();
        }

        public Document Document
        {
            get { return (Document)GetValue(DocumentProperty); }
            set { SetValue(DocumentProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Document.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DocumentProperty =
            DependencyProperty.Register("Document", typeof(Document), typeof(ViewerControlEx), new PropertyMetadata(null, new PropertyChangedCallback(OnDocumentChanged)));

        private static void OnDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctrl = d as ViewerControlEx;
            if (ctrl == null) return;

            ctrl.viewer.Document = e.NewValue as Document;
        }

    }
}
```

処理の内容としては、

- DocumentPropertyというDependancyPropertyを作成し、変更されるとOnDocumentChangedが呼びます。
- ViewerControlのDocumentプロパティを差し替えています

### 自作のVB-Reportのビューア(ViewerControlEx)を配置する

- ここでは、メインの画面に配置していますが、あとは自由です。
- Report作成ボタンを押すとプレビューが出るようしました。

```xml:MainUcView.xaml
<UserControl
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             xmlns:vbr="clr-namespace:AdvanceSoftware.VBReport8.WPF;assembly=VBReport8.WPF.Viewer" x:Class="VbReportSampleApp.Views.MainUcView"
             xmlns:vw="clr-namespace:VbReportSampleApp.Views"
             prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="60"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="10">
            <Button Content="Report作成" Command="{Binding CreateReportCommand}"  CommandParameter="{Binding ElementName=viewerControlEx}" Width="200"/>
        </StackPanel>
        <vw:ViewerControlEx x:Name="viewerControlEx" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Document="{Binding DocumentReport}"/>
    </Grid>
</UserControl>
```

- 自作のVB-Reportのビューア(ViewerControlEx)を介することでDocumentにBindingしてもエラーにならなくなります。

### 自作のVB-Reportのビューア(ViewerControlEx)を配置した画面のViewModelの作成

- 今回は、メインの画面に配置
- ※ 帳票の作成には、アドバンスソフトウェア社のサンプルのファイルをそのまま使用しています。

```csharp:MainUcViewModel.cs
using AdvanceSoftware.VBReport8;
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Xml;

namespace VbReportSampleApp.ViewModels
{
    public class MainUcViewModel : BindableBase
    {
        public MainUcViewModel()
        {
            
        }

private DelegateCommand<ViewerControlEx> createReportCommand;
        public DelegateCommand<ViewerControlEx> CreateReportCommand =>
            createReportCommand ?? (createReportCommand = new DelegateCommand<ViewerControlEx>(CreateDocumentExecute));

        private void CreateDocumentExecute(ViewerControlEx viewerControlEx)
        {
            viewerControlEx.viewer.Clear();
            DocumentReport = CreateDocument();    
        }
        
        public Document CreateDocument()
        {
            XmlDocument doc = new XmlDocument();
            XmlNodeList list;
            
            string userID;
            string designFilePath = @"見積書.xls";
            cellReport1.FileName = designFilePath;
            cellReport1.Report.Start();
            cellReport1.Report.File();

            // 請求リストを読み込みます。
            doc.Load(@"見積書リスト.xml");
            list = doc.SelectNodes("/見積書リスト/顧客");

            foreach (XmlNode node in list)
            {
                int totalSeikyu = 0;
                userID = node.SelectSingleNode("@id").InnerText;

                cellReport1.Page.Start("見積書", "1");
                // 日付
                cellReport1.Cell("**Date").Value = DateTime.Now;
                // 見積有効日
                cellReport1.Cell("**EndDate").Value = (double)DateTime.Now.ToOADate() + 7;
                // 見積書番号
                cellReport1.Cell("**Mitsumori").Value = node.SelectSingleNode("./見積書番号").InnerText;
                // 会社名
                string officename = node.SelectSingleNode("./会社名").InnerText;
                if (officename != "")
                {
                    cellReport1.Cell("**OfficeName").Value = node.SelectSingleNode("./会社名").InnerText;
                }
                // 部署名
                string sectionname = node.SelectSingleNode("./部署名").InnerText;
                if (sectionname != "")
                {
                    cellReport1.Cell("**SectionName").Value = node.SelectSingleNode("./部署名").InnerText;
                }
                // お客様名
                cellReport1.Cell("**UserName").Value = node.SelectSingleNode("./お客様名").InnerText + " 様";
                // 郵便番号
                cellReport1.Cell("**Post").Value = "〒" + node.SelectSingleNode("./郵便番号").InnerText;
                // 住所県
                cellReport1.Cell("**Address1").Value = node.SelectSingleNode("./住所県").InnerText;
                // 住所その他
                cellReport1.Cell("**Address2").Value = node.SelectSingleNode("./住所その他").InnerText;
                // 商品一覧
                int y = 0;
                XmlNode shyouhinNode = node.SelectSingleNode("./商品明細");
                XmlNodeList list2 = shyouhinNode.SelectNodes("./商品");

                // 一括割引対象品と除外品を判断するためのフラグ("1"→"0"に変更した時点で割引額の表示)
                string discount = "";
                string discountPrev = "";

                foreach (XmlNode node2 in list2)
                {
                    discount = node2.SelectSingleNode("./一括割引").InnerText;
                    // 一括割引対象品でなくなった時点で、割引額を求める
                    // 全て割引除外品の場合、計算を行わない
                    if ((discount == "0") && (discountPrev == "1"))
                    {
                        // 合計額(税抜き)が、100万未満の場合、一括割引を行わない。
                        if (totalSeikyu >= 1000000)
                        {
                            cellReport1.Cell("**Shouhin", 0, y).Value = "（一括割引）";
                            cellReport1.Cell("**Shouhin", 0, y).Attr.FontColor2 = AdvanceSoftware.VBReport8.xlColor.Red;
                            int discountGaku = totalSeikyu * -2 / 10;
                            cellReport1.Cell("**Kingaku", 0, y).Value = discountGaku;
                            cellReport1.Cell("**Kingaku", 0, y).Attr.FontColor2 = AdvanceSoftware.VBReport8.xlColor.Red;
                            totalSeikyu += discountGaku;
                            cellReport1.Cell("**Biko", 0, y).Value = "上記合計金額より２０％ＯＦＦ";
                            y += 2;
                            cellReport1.Cell("**Shouhin", 0, y).Value = "（以下、割引除外品）";
                            y++;
                        }
                    }
                    // 商品名
                    cellReport1.Cell("**Shouhin", 0, y).Value = node2.SelectSingleNode("./商品名").InnerText;
                    // 数量
                    int unitNumber = Convert.ToInt32(node2.SelectSingleNode("./数量").InnerText);
                    cellReport1.Cell("**Suuryou", 0, y).Value = unitNumber;
                    // 単価
                    int unitPrice = Convert.ToInt32(node2.SelectSingleNode("./単価").InnerText);
                    cellReport1.Cell("**Tanka", 0, y).Value = unitPrice;
                    // 金額
                    int totalPrice = unitNumber * unitPrice;
                    cellReport1.Cell("**Kingaku", 0, y).Value = totalPrice;
                    totalSeikyu += totalPrice;
                    // 備考
                    cellReport1.Cell("**Biko", 0, y).Value = node2.SelectSingleNode("./備考").InnerText;
                    y++;
                    discountPrev = discount;
                }
                // 税抜
                cellReport1.Cell("**Zeinuki").Value = totalSeikyu;

                // 消費税
                int tax = (int)(totalSeikyu * 0.05);
                cellReport1.Cell("**Zei").Value = tax;

                // 税込
                cellReport1.Cell("**Zeikomi").Value = totalSeikyu + tax;

                cellReport1.Page.End();
            }
            cellReport1.Report.End();
            return cellReport1.Document;
        }

        private Document documentReport;
        public Document DocumentReport
        {
            get
            {
                return documentReport;
            }
            set
            {
                SetProperty(ref documentReport, value);
            }
        }

        private CellReport cellReport1 = new CellReport();
        
    }
}
```

## 実行結果
![VB-Reportgif.gif](https://qiita-image-store.s3.amazonaws.com/0/90597/f43cb54d-348b-7a0f-8b34-ef03b748ecde.gif)

## まとめ
MVVMパターンで行うならば、VB-Reportの場合は、DependancyPropertyを作成したユーザーコントロールを介してバインディングを行うことで解決します。
Prismを使ったソリューションの場合にViewModelに固執するわけではありませんが、むやみやたらとコードビハインドで記述しないで済む方法があるということです。


## 追加補足
2017/09/04
アドヴァンスソフトウェア社から連絡があり、
> CellReport のオブジェクトによる 2回目の帳票生成を
行う場合は、1回目の帳票ドキュメントがビューアコントロールに紐付けられて
いるため、予め viewerControl.Clear メソッドを呼び出して頂く必要がございます。

という連絡がありましたので、ボタンコマンドにコマンドパラメータでElementNameを渡して、動作するように修正しました。

ただ、以下のような修正でもよいかもしれません。

```csharp:ViewerControlEx.xaml.cs
private static void OnDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var ctrl = d as ViewerControlEx;
    if (ctrl == null) return;

    ctrl.viewer.Clear(); /* ドキュメントのクリア */
    ctrl.viewer.Document = e.NewValue as Document;
}
```

- 単純にプレビューを行いたのであれば、ドキュメント変更イベントにctrl.viewer.Clear();というのを追記すれば、ドキュメントの変更都度クリアされるので、あえてElementNameをコマンドパラメータを渡すより良いかもしれません。