Azure Function から Redmine の API を呼び出す

サーバーレスな Azure Functions の練習がてら、Redmine の API を呼び出してみるテストを晒しておきます。Azure Functions の詳しい説明は、https://docs.microsoft.com/ja-jp/azure/azure-functions/ を参考にしてもらうとして、一番手軽なのは HttpTrigger である。普通の Web API と同じように URL アドレス経由で呼び出して、指定のファンクションを実行する。「ファンクション」とは言っても、F# とか Scala のような「関数型」のファンクションではないのだけど、ある意味「ステートレス」ということでは、AWS の Lambda もファンクションだし、Azure Functions もファンクションなので、「関数」って訳語が付けられているのだが。果たしてこれは、正しいのか?ってのは微妙だ。

ローカルにFunction Appを作る

Function Appは、Azure 上で作って運用するのだけど、実はローカルなPC上でもAzure Functionsを実行できる。

Azure Functions Core Tools のインストール
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local

をインストールすると、ローカルPCでテスト実行ができるようになる。
実は、このツールは、Visual Studio でAzure Functionsのプロジェクトを作って実行すると、自動でインストールされる。

ちょうど、ASP.NET Core MVC のアプリをテスト実行したときと同じように、.NET Core 上のコマンドが動いてAzure Functionsのエミュレータが起動する。

ファンクションを作る

関数の作り方は非常に簡単だ。ちょうど、ASP.NET MVC の Controller にメソッドを作るようにファンクションを作っていく。ASP.NET MVC の Controller と違うのは、static クラスに static メソッドとして定義されているところだ。
ASP.NET MVC の場合も HTTPプロトコルでステートレスなのだから、意味あいとしは同じと言えば同じなのだが。ひとまず、Azure側からstatic関数として呼び出されることになる。だから「ステートレス」っていのが明確になっている。

public class App
{
    public const string ApiKey = "<api_key>"
    public const string BaseUrl = "<redmine_url>";
}

public static class ProjectFunc
{
    private static string ApiKey = App.ApiKey;
    private static string BaseUrl = App.BaseUrl;
    private static HttpClient client = new HttpClient();
    private static int _count = 0;  // 呼び出しカウンタ

    /// <summary>
    /// プロジェクトリストを取得
    /// </summary>
    /// <param name="req"></param>
    /// <param name="log"></param>
    /// <returns></returns>
    [FunctionName("GetProjects")]
    public static IActionResult GetList(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "Project/List")]
            HttpRequest req, TraceWriter log)
    {
        var json = client.GetStringAsync($"{BaseUrl}/projects.json?key={ApiKey}").Result;
        _count++;
        log.Info($"count: {_count}");
        var data = JsonConvert.DeserializeObject<ProjectList>(json);
        foreach ( var proj in data.projects )
        {
            log.Info($"{proj.id}: {proj.name}");
        }
        return new OkObjectResult(json);
    }

    [FunctionName("GetProject")]
    public static IActionResult Get(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "Project/Get")]
            HttpRequest req, TraceWriter log)
    {
        string id = req.Query["id"];
        var json = client.GetStringAsync($"{BaseUrl}/projects/{id}.json?key={ApiKey}").Result;
        var proj = JsonConvert.DeserializeObject<Project>(json);
        log.Info($"{proj.id}: {proj.name}");

        return new OkObjectResult(json);
    }
}

HttpTrigger は URLアドレス経由で呼び出されるので、URLはFunction Appで使われる API KEY を含めて、

https://sample-azfunc-dotnet.azurewebsites.net/api/HttpTrigger1?code=xxxxxxxx

な形で呼び出されるのだが、ここではローカルのテスト環境なのでcodeを省略して、

http://localhost:7071/api/Project/List

のように呼び出すことができる。
実は、呼び出すときの関数名は FunctionName 属性で設定するのだけど、この属性でフォルダーを掘ることができない。引数についている HttpTrigger.Route にフォルダー付きの呼び出しを付けられるので、これを使うと Project/List とか Project/Get?id=100 のような Web API っぽい形に変えられる。

Redmine の JSON をクラスにする

Redmine を API で呼び出すと、JSON形式あるいはXML形式で受け取ることができる。以前は、これを手作業でクラスに直していた(コンバーターも手で作ってた)のだが、Visual Studio で「編集」→「形式を選択して貼り付け」を使うと、元のJSON形式のデータから、
C#のクラスに直すことができる。

ルートのノードが「Rootobject」となるので、適宜「ProjectList」とか「ProjectItem」とかに直してやる。
これと、JsonConvert.DeserializeObject メソッドを組み合わせると、JSON形式のデータを一気にクラスに直せる。

実行してみる

Visual Studio 上でデバッグ実行をして、

ブラウザから、http://localhost:7071/api/Project/List を実行すると、別途 Redmine が動いているサーバーに接続して、JSON形式で受け取ることができる。

各種 WEB API の呼び出し形式がまちまちだったりするので、こんな風にFunction Appを使って、統一的にアクセスできるようにコンバートすると GUI 側の変更が楽になるのではないかな、と試作&思索しているところ。

カテゴリー: 開発, Azure Functions, C# | Azure Function から Redmine の API を呼び出す はコメントを受け付けていません

ClosedXMLとMVVMパターンを良好な関係にしてみる

MVVMパターンを考えるとき、ViewはXAMLとかGUIだろうという固定観点があるが、実はそうではない。と思うのだがどうだろうか?いわゆる、ASP.NET MVC の View にWeb APIを割り当てると View ってのは XML や JSON になる。クライアントから見れば XMLやJSON はデータだが、MVCパターンで作られたWebアプリにしてみれば、XMLやJSONをViewに見立てることができる。実際、Web APIを作るとき複雑なXMLを返す場合は、Viewの機能を使ったほうが楽だったりする。

ClosedXMLをViewに見立てる

CloseXMLを使って、Excelに値を書き込もうとするとき Cell(row,column)あるはCell(“A1”)を使うわけだけど、ここの書き込み先をXAMLのバインドのように書けないだろうか?ってのを思いついた。

で、ClosedXML.Report https://github.com/ClosedXML/ClosedXML.Report はテンプレート用のExcelを作っておいて、あらかじめ名前を付けておく。

確かに、こんな風に Excel に名前を付けておいて、そこを目指して書き込むのもいいんんだが、なんらかのマークを書いておかなければいけないのが微妙な感じがする。実は、XAML も {Binding propName} という形で XAML 自身に記述するので似たようなことをやっているのだが。

これ、マークを View のほうに書いているけど、じゃあ ViewModel のほうに書くのはどうか?とおもってみたのがこれ。

テンプレートとなるExcelはふつうに記述しておく。

書き込んだ結果はこっち

Excelへの書き込み位置は、ViewModelに属性として記述する。

public class ViewModel : ObservableObject
{
    private string _Name, _Address;
    private string _ModifiedDate;

    [ExcelBinding(Address: "B2", Property: "Value")]
    public string Name { get => _Name; set => SetProperty(ref _Name, value, nameof(Name)); }
    [ExcelBinding(Address: "B3", Property: "Value")]
    public string Address { get => _Address; set => SetProperty(ref _Address, value, nameof(Address)); }
    // 更新日時
    [ExcelBinding(Address: "A4", Property: "Value")]
    public string ModifiedDate { get => _ModifiedDate; set => SetProperty(ref _ModifiedDate, value, nameof(ModifiedDate)); }
}

ViewModelからClosedXMLを通してExcelに書き込むコードはこんな感じ。

ExcelBind _bind;
ViewModel _vm;
public void LoadExcel()
{
    string tableName = "Personal";
    string path = @"C:\Users\masuda\Documents\サンプルテンプレート.xlsx";
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.FirstOrDefault(t => t.Name == tableName);
        _bind = new ExcelBind(sh);
        _vm = new ViewModel();
        _bind.DataContext = _vm;

        _vm.Name = "Tomoaki Masuda";
        _vm.Address = "Itabash-ku Tokyo in Japan";
        _vm.ModifiedDate = DateTime.Now.ToString();     // 更新日を表示
        wb.SaveAs(@"C:\Users\masuda\Documents\sample_output.xlsx");
    }
}

当然のことながら、ClosedXML にはバインディング処理はないので、ClosedXMLのXLWorkbookとIXLWorksheetをラップするような橋渡しのExcelBindクラスを作る。ExcelBindクラスにはMVVMパターンのDataContextプロパティを持っているので、これにバインドする。

ViewModel の各種プロパティに設定をすると INotifyPropertyChanged インタフェースを使って、View に見立てられた ClosedXML に値を書き込むという仕組み。ClosedXML は入力インターフェースを持たないので、ClosedXML からの TextChenge などのイベント受信は不要(実は自動計算などがあるから、無いわけではないのだが)というわけだ。
ViewModel クラスは普通の MVVM パターンと同じようにプロパティに値を設定すればよい。

内部実装

ざっと内部的な実装を

目的のセルを設定するExcelBinding属性

“A1″や行列で指定する。将来的にはセルの名前を設定できるのもよいだろう。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class ExcelBindingAttribute : Attribute
{
    public string SheetName { get; }
    public string Address { get; }
    public int Row { get; }
    public int Column { get; }
    public string PropertyName { get; }
    public ExcelBindingAttribute(string Sheet, string Address, string Property )
    {
        this.SheetName = Sheet;
        this.Address = Address;
        this.PropertyName = Property;
    }
    public ExcelBindingAttribute(string Sheet, int Row, int Column, string Property )
    {
        this.SheetName = Sheet;
        this.Row = Row;
        this.Column = Column;
        this.PropertyName = Property;
    }
    public ExcelBindingAttribute(string Address, string Property = "Value") 
    {
        this.SheetName = "";
        this.Address = Address;
        this.PropertyName = Property;
    }
    public ExcelBindingAttribute(int Row, int Column, string Property = "Value")
    {
        this.SheetName = "";
        this.Row = Row;
        this.Column = Column;
        this.PropertyName = Property;
    }
}

橋渡しをする ExcelBind クラス

内部的にClosedXMLのXLWorkbookかIXLWorksheetを持たせる。DataContextプロパティに設定した ViewModel の PropertyChanged イベントをフックして、ClosedXML 経由で目的のセルの値を変更する。プロパティは、簡易のため Value のみに固定している。

GetDataContext メソッドは、逆向きで Excel シートから ViewModel を作るときに使う。

public class ExcelBind
{
    private XLWorkbook _workbook;
    private IXLWorksheet _worksheet;

    public ExcelBind(XLWorkbook book) { _workbook = book; }
    public ExcelBind(IXLWorksheet sheet) { _worksheet = sheet; }


    private object _DataContext = null;
    public object DataContext
    {
        get => _DataContext;
        set
        {
            if (_DataContext != value)
            {
                _DataContext = value;
                if ( _DataContext is INotifyPropertyChanged )
                    (_DataContext as INotifyPropertyChanged).PropertyChanged += _DataContext_PropertyChanged;
            }
        }
    }
    private void _DataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (_DataContext == null) return;
        // 1.ViewModelのプロパティ名からリフレクションで値を取得
        var pi = _DataContext.GetType().GetProperty(e.PropertyName);
        if (pi == null) return;
        // 2.属性から設定先のClosedXMLのプロパティを取得
        var attr = Attribute.GetCustomAttribute(pi, typeof(ExcelBindingAttribute)) as ExcelBindingAttribute;
        // 3.設定先にリフレクションで値を代入
        try
        {
            if (string.IsNullOrEmpty(attr.Address))
            {
                // Row,Column 指定の場合
                if (_worksheet != null)
                    _worksheet.Cell(attr.Row, attr.Column).Value = pi.GetValue(_DataContext);
                if (_workbook != null && _workbook.Worksheet(attr.SheetName) != null)
                    _workbook.Worksheet(attr.SheetName).Cell(attr.Row, attr.Column).Value = pi.GetValue(_DataContext);
            }
            else
            {
                // Address 指定の場合
                if (_worksheet != null)
                    _worksheet.Cell(attr.Address).Value = pi.GetValue(_DataContext);
                if (_workbook != null && _workbook.Worksheet(attr.SheetName) != null)
                    _workbook.Worksheet(attr.SheetName).Cell(attr.Address).Value = pi.GetValue(_DataContext);
            }
        }
        catch { } // 例外は無視する
    }

    public void SetDataContext(object context) { _DataContext = context ; }
    public T GetDataContext<T>( T o = null ) where T : class, new() {
        if ( o == null ) o = new T();
        // バインド先のデータを workbook/worksheet から取り出す
        // 1.プロパティ一覧を取得
        var props = o.GetType().GetProperties();
        foreach ( var pi in props )
        {
            // 2.ExcelBindingAttribute属性がついているプロパティを取り出す
            var attr = Attribute.GetCustomAttribute(pi, typeof(ExcelBindingAttribute)) as ExcelBindingAttribute;
            if ( attr != null )
            {
                IXLWorksheet sh = _worksheet;
                if ( sh == null )
                {
                    if (_workbook != null && _workbook.Worksheet(attr.SheetName) != null)
                        sh = _workbook.Worksheet(attr.SheetName);
                }
                if ( sh == null ) { return o; }

                // 4.ViewModel のプロパティに値を設定する
                if (string.IsNullOrEmpty(attr.Address))
                {
                    pi.SetValue(o, sh.Cell(attr.Row, attr.Column).Value);
                }
                else
                {
                    pi.SetValue(o, sh.Cell(attr.Address).Value.ToString());
                }
            }
        }
        return o;
    }
}

逆に Excel シートから ViewModel を構成する

ExcelBindのGetDataContextメソッドを使って、ViewModelを構成する例。これを使うと、Excel シートに記入してもらって、そこから値を取り出すのが楽にできるのでは?と考えたりする。

private void clickGetFromExcel(object sender, RoutedEventArgs e)
{
    string tableName = "Personal";
    string path = @"C:\Users\masuda\Documents\sample_output.xlsx";
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.FirstOrDefault(t => t.Name == tableName);
        _bind = new ExcelBind(sh);
        // ViewModel を再構成
        var vm = _bind.GetDataContext<ViewModel>();

        System.Diagnostics.Debug.WriteLine(vm.Name);
        System.Diagnostics.Debug.WriteLine(vm.Address);
        System.Diagnostics.Debug.WriteLine(vm.ModifiedDate);

        MessageBox.Show(vm.Name);
    }
}

ViewModelからPropertyChangedを受ければViewになるのか?

直接 ClosedXMLに渡すわけではないが、ExcelBindクラスを媒介して値のやり取りができている。WPFやUWPのようなユーザーインタフェースはないが、ViewModelクラスのプロパティを使ってView(この場合はExcelシート)にアクセスできている。
この部分、構造が簡単な XML ならばデータとして扱うのだが、ExcelのOpenXML形式のように複雑怪奇なXMLの場合、ピンポイントで修正することを考えると「View」として扱うのがベターではないかと思っている。
ViewModelのプロパティは、別途DIなどでXMLやJSONから読み込むようにすれば、消えてしまった InfoPath のようにできるのではないな、と。

このあたりを少し整理して、NuGet に挙げられるようにする予定。

カテゴリー: 開発, C# | ClosedXMLとMVVMパターンを良好な関係にしてみる はコメントを受け付けていません

ClosedXMLを使って、超高速にリスト形式の帳票を作成する

xlsx 形式な Excel ファイルを高速に読み込めたということは、ひょっとして高速に書き込めるのでは?と思って書いてみたのがこれ。
あらかじめ Excel で作ったテンプレートを用意しおいて、行を追加しているだけ。行数分コピーしているのは3行目のセル/行に書式が設定してあるから。こうしておくと罫線とか色とかフォントとかをコードで指定しなくて済む。

public void 印刷()
{
    var path = AppDomain.CurrentDomain.BaseDirectory + @"template\協力会社一覧.xlsx";
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.First();
        // あらかじめ行数分用意しておく
        var rg = sh.Row(3);
        for (int i = 0; i < this.Items.Count - 1; i++)
        {
            rg.CopyTo(sh.Row(i + 4));
        }

        int r = 2;
        foreach (var it in this.Items)
        {
            sh.Cell(r, 1).Value = it.業者ID;
            sh.Cell(r, 2).Value = it.業者名称;
            sh.Cell(r, 3).Value = it.担当者;
            sh.Cell(r, 4).Value = it.役職;
			// 途中は省略
            r++;
        }
        // テンポラリファイルに保存
        var temp = System.IO.Path.GetTempFileName() + ".xlsx";
        wb.SaveAs(temp);
        System.Diagnostics.Debug.WriteLine(temp);
        // テンポラリファイルを開いて印刷
        var xapp = new Excel.Application();
        var xwb = xapp.Workbooks.Open(temp);
        var xsh = xwb.Worksheets[1] as Excel.Worksheet;
        xsh.PrintOutEx();
        xwb.Close(false);
        xapp.Quit();
        System.IO.File.Delete(temp);
    }
}

どうやら、ClosedXML には印刷機能がないらしいので、印刷のほうは Microsoft.Office.Interop.Excel を呼び出している。それでもスピードは体感で10倍以上にはなる。書き込みも相当スピードアップされるらしい。
実はレポート出力用に ClosedXML.Report https://github.com/ClosedXML/ClosedXML.Report というのもあるのだが、行単位の一覧程度ならばこの方式で十分だろう。表形式じゃないレポート形式の場合は、別途変換してみよう。

実行例

テンプレート用の Excel ファイルはこんな感じで1行だけ作っておく。フォントの設定とか文字列の折り返し、罫線などをあらかじめ Excel 上で設定してあるので、3行目を CopyTo するだけでよい.

これを Excel COM を使って印刷する。OpenXML/ClosedXML が印刷機能を持っていればよいのだが、どうやら XML の読み書きの機能だけでレンダリングはないらしい。実際は PDF に落とせばよい(PDFから印刷する方法もあるので)ので、iText とかの PDF 出力を使えばよいらしいのだが、まあ、印刷はプリンタ独自の設定も含むことがあるので Excel COM を使ったほうがよいだろう。

Windows 10 で印刷した結果がこちら。Excel からの出力先を PDF にすると手軽に PDF ファイルの落とせる(Excel のエクスポート機能を使ってもよい)。

帳票は Excel 形式で残したほうがよい場合と、Excel 形式のような「編集できる形式」では残してはいけない場合がある。編集不可にしたいときは PDF にするのが常なのだが、これは税務処理などで金額の修正があると困る場合によく使われる。Excel 形式のまま残すと改竄されてしまうので、わざわざ PDF で残すのだ。もっとも、詳しい人ならば暗号化していないと PDF の内部で修正が出来てしまうのだが…まあ、一般的には「PDF だと修正できない」ので大丈夫と思ってよい。きちんとやる場合は PDF に暗号をかけるか、Excel 形式のままハッシュ値を保存して暗号化(いわゆるブロックチェーンな方法)をとればよい。

 

カテゴリー: C# | ClosedXMLを使って、超高速にリスト形式の帳票を作成する はコメントを受け付けていません

MVVM パターンでリモート操作の設計を考察

以前から考えていることに、MVVM パターンの ViewModel を使った遠隔操作がある。そもそも MV* パターン(MVVM とか MVC とか)の場合、View を独立させることができるので、View から離れている ViewModel や Model は複数の View に対応していてもよいだろう、という発想がある。Visual Studio のプロジェクトテンプレートだと、ひとつの View に対してひとつの ViewModel が対応するように作られるけど。まあ、それはそれとして。

image

MVVM パターンの View が GUI 以外にあるのだろうか?と考えてみたのだが、そのほかはないだろう。MVC パターンを使って Web API を作ることはよくあるけど、Web API や RESTful 程度に整理されてしまうと、XAML の入れ子の部分とか複雑怪奇なリストが作れるとかの意味がなくなってしまう。データストアとしての XAML と使う以外の場合、データバインドやイベント/Command パターンを使う場合は、もっとランダムにアクターからのイベントが発生するときに XAML の構造が力を発揮する。アクセス方法が簡単あるいは整理されているならば、わざわざ View からイベントを取る必要がない(イベント駆動ではないのだから)。

というわけで、ひとまずランダムかつインタラクティブな入力としての View/XAML を考えるうえで、GUI を対象にする(インタラクティブなものとして、複数のセンサー等も考えてみたけど、それも XAML で扱うほど複雑ではない気がする)。

image

逆に言えば、人が手入力をして、人の目で確認して、再び人が手入力するという手順は、きわめて「遅い」手段である。例えば、ロボット入力(RPAを含む)ならば、多少回りくどい手順を追加しても確実に入力できる方法を取ることができる。だが、人力の場合は、間違いがあり間違いを目で確かめて、再入力するという手間がかかる。これこそが GUI の特徴でもある…ということにしておこう。

XAML を動的入れ替え&VMのリモート化

動的に XAML が入れ替えられると仮定しよう(実際、WPFの場合、XamlReaderを使うと入れ替えられる)。動的に XAML を生成しなくても、画面遷移の中で View を切り替える場合も同じ方法になる。

image

ひとつの VM に対して、複数の View を持つ。ここの利点は、

  • 後から XAML を入れ替えることができる(動的に更新が可能)
  • 画面遷移のときに、複数の View のメモリを共有している

というものがある。動的入れ替えのほうは、ブラウザから Webサーバーにアクセスする Web アプリケーションに似ている。ただし、Web アプリの場合は、内部データをサーバー側のセッションに持つことが多いのだが、ここでは VM 上に持たせる。SignalR はこれを JavaScript で実現したものだ。表面上の View はいわゆる「ユーザーインターフェース」なので頻繁に変わることがある。ユーザーによるカスタマイズもあるし、見た目を変えたバージョンアップもある。View > ViewModel > Model の順に更新頻度が変わるのがベターだ。

そして、ViewModel 同士をインターネット回線で通信させる。従来ならば View にべったりくっついた ViewModel を作るところだが、本来の MVVM パターンを意識するならば、クライアント/サーバーの2つの ViewModel に分離することが可能なはずだ。通信回線が遅いという前提もあるのだが、ここではそれなりに早い回線であると仮定しよう。

幸いにしてかつここが MVVM パターンの最大の特徴なのだが、View と ViewModel とのインターフェースは、INotifyPropertyChanged と ICommand を実装する(面倒なので、ICommand のほうは省いてしまうことが多いけど)。この部分の実装は定型のパターンがあるのだけど、実際は独自に作っても良い。このインターフェースを利用して VM 同士の相互通信を可能にすればよい。アスペクト指向の変型判と言っても良い。

XAML の動的入れ替えのみ

話をもうちょっと簡単にするために、要素をひとつずつみてみよう。

image

たとえば、同じ ViewModel を複数の View/XAML で共有するパターンを考える。この場合、画面遷移をおこなっても同じ VM を共有するので内部的なコピーが発生しない。View のコンストラクタに対して ViewModel のオブジェクトを引き渡すだけでよい。通常は引き渡すときに ID のみとかを渡すところなのだが、List-Detail の画面遷移だと、List 側に既に Detail の情報を持っていることが多い。内部的に Detail の Item だけを渡してもいいのだけど、新規作成や削除などで元の List を変更することを考えれば、List 自体を引き渡してしまってもよい。当然、画面から戻ってきたときに追加/削除することもできるけど、Xamarin.Forms や UWP の場合にはダイアログ形式で遷移するか画面を切り替えるかで作りが違うので、ViewModel の動きを統一したいときには、モーダル/モードレス関係なく遷移先に子画面で List の更新をしてしまったほうが問題が少なかったりする。

となれば、動的に「未知な」 View/XAML を切り替えたときに、ひとつの ViewModel で対応するにはどうしたらいいだろうか?という問題がある。

  • 未知な View に対して、常に ViewModel を更新する
  • 未知な View のエラー部分は、ViewModel 側で無視する

という2つのパターンがある。前者のほうは正統ではあるのだが、頻繁に更新されうる View に対して頻繁に ViewModel を更新するのはあまり適切ではない。現在の ViewModel にあわせて View を構成するのも筋ではあるのだが(きちんと制御された View ならそうだろう)。

ならば、ちょっと緩い形で実装された View(間違いがある HTML 形式のようなものだ)に対して、緩く ViewModel が対応してくれてもよい。なので、例外が出てもフェールセーフで動いてくれる ViewModel が必要になるし、動的切り替えの XamlReader では例外が出ないことが望ましい。現状の XamlReader では、プロパティやイベント先がないと例外が発生してしまうので、この用途には向かない。

複数の VM を複数クライアントで共有する

MVVM パターンの ViewModel が、クライアントとサーバーで分離できると仮定するならば、別々の PC で動いている View を手軽に共有できるだろう。この共有度ぐあいはネットワークのスピードに依るのだが、プロパティの変更イベントと View への操作のコマンド制御が通れば、うまくいくと思われる。

image

このように分離していくと、View 内のコード(いわゆるコードビハイド)の意味が明確になってくる。

  • 特定の View 内で閉じるものは、コードビハイドでよい。
  • Model や他PC に影響を与えるもの/与えられるものは、ViewModel に記述する

という使い分けになる。

例えば、画面をクリックしたときに色を変えるとか、数値のフォーマットを変えるとかを XAML の Converter を作ることが多いのだが、これをコードビハイドで実装しても問題ないことがわかる。ひとつの PC 上のひとつの View で閉じているからだ。実装的には、XAML に付随させるほうがよいので JavaScript のような View に埋め込まれるスクリプト言語を使ったほうが便利だろう(アセンブリの配布は少々面倒なので)。逆に、Model に影響を与えるようなロジックは ViewModel に実装せざるを得ず、この部分は通信を含めたコードになる。

異なる PC で View を同期させるためには、サーバー側の VM にはブロードキャスト的な通信が必要になる。このブロードキャストは、INotifyPropertyChanged のインプリメントとして書かれるだろうから、利用時に強く意識されることはないはずだ。このあたりはアスペクト指向的に属性で記述することもできれば、なんらかの実装クラスを継承して使うことになる。少なくとも、本来の ViewModel の実装と、通信を行うための実装を分離する必要がある。

その先は

ひとまず、思考実験はここまで。XamlReader の再実装のほうは、.NET Core 化された WPF のコードを利用すればいいと思う。以前 Xamarin.Forms で試してみて例外をトラップするところまではできたので。リモート可能な ViewModel のほうは、アスペクト指向のように属性でやってみたいところだけど、このあたりは仮コードを作ってみることにしよう。

カテゴリー: 設計 | MVVM パターンでリモート操作の設計を考察 はコメントを受け付けていません

ClosedXMLを使って、超高速にExcelからSQL Serverへデータ転送する

SQL Serverのデータをバックアップするとき正式な方法ならば、SQL Serverのバックアップ機能を使ってバックアップ/復元をすればよい。のだが、別な場所のデータベースに移したいとき使うにはいささか面倒くさいことが起こる。相互のバージョンが同じならば、そのあっまバックアップ/復元を繰り返せばよいのだが、ちょっとでもバージョンが違うとエラーになってしまう。仕方がないので、Accessを媒介する方法をよくとるのだが、型が微妙に違っているので(特に文字列の長さが異なってエラーになる)若干の手動の調節が必要になってくる。MySQLならば、ダンプコマンドを使ってえいっとSQLに直してしまうところなのだが、SQL Serverにはそんなものはない。

Excelを媒介にしてみる

適当なバックアップツールを作るのもよいのだが、ここは Excel を使って媒介してみよう、と考えた。SQL Server Management Studioにはデータのエクスポート先に、AccessやExcelを選ぶことができる。当然、Excel からインポートすることもできるのだが、ID の設定でちょっと手動なところがある。できることならば、一発でインポートしたいところだ。それに、Access じゃなく Excel でインポートできれば、ちょっとした修正ならば Excel で修正した後にインポートし直すということができる。いわゆるマスターテーブルの修正を、Excel で書き込んで、SQL Serverに書き戻すことができれば便利だろう、と思たたわけなのだが。

試しに Microsoft.Office.Interop.Excel を使って Cells で参照しながらちまちま読み込んで、SqlBulkCopy を使って一気にインポートすれば高速になるだろう、と思ったのだが…ああ、とてつもなく遅い。どうも COM 経由で Cells を参照しているところがむちゃくちゃ遅くて、1000件位の読み込みでも5,6分はかかってしまうという体たらくだ。たった1万件でも1時間程度かかってしまう見込みなので、これはちょっと実用に耐えない。
ちなみに、Range を使って二次元配列に読み込んでという方法を使うと高速化できるのだが、ちょっとセルをアクセスをすると遅くなるのと、デバッグ実行などで COM な Excel が残ったりして結構面倒なことが多い。
Range を二次元配列で読み取る方法は、後日示しておきたい。手軽な方法ではあるので。

ClosedXML に切り替える

XML形式で保存される xlsx のほうならば、Open XMLが使える。どうやら ClosedXML は内部的にOpen XML SDK を使っているそうなので、これを使ってみる。生の OpenXML を使うよりも断然 ClosedXML のほうが使いやすいという記事があるので、参考にさせてもらう。

【C#】Excelの取り扱いにClosedXMLを使用する – あたも技術ブログ
http://atamo-dev.hatenablog.com/entry/2017/07/23/180026

ClosedXMLを使った、Excel操作の例 ClosedXML
http://closedxml.codeplex.com/ https://gist.github.com/ishisaka/6128639

果たしてどれだけスピードアップするのか?と思って試してみたのだが、体感的には Microsoft.Office.Interop.Excel より ClosedXML のほうが100倍位早い。最初に xlsx ファイルを開くときには結構時間が掛かる(と言っても10秒位)。おそらく XML ファイル全てを読み込みパースするので時間が掛かっているのだろうけど、全体でかかる時間からすれば全然大したことはない。

ClosedXML で読み込んで SqlBulkCopy する

Excel から書き戻すのが目的なので、INSERTにはSqlBulkCopyを使う。SqlBulkCopyに渡すのはDataTableになるので、EFのエンティティクラス(単純な値クラス)からDataTableオブジェクトに変換した関数を使う。

NuGet で ClosedXML を取り込んで、XLWorkbook クラスで WorkBook を読み込む。

using ClosedXML.Excel;

private void clickReadClosedXml(object sender, RoutedEventArgs e)
{
    // 0.テーブル名を指定
    string tableName = "営業業務データ";
    // 1.Excel を開く
    string path = @"営業管理_20190201.xlsx";
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.FirstOrDefault(t => t.Name == tableName);
        // 2.シートからEFに読み込み
        var rc = new ClosedXmlRangeConverter<営業業務データ>(sh);
        var items = rc.ToList();
        // 3.データベースの「営業業務データ」に書き込み
        var ent = new testdbEntities();
        var dt = items.AsDataTable();
        var cn = ent.Database.Connection as SqlConnection;
        var bc = new SqlBulkCopy(cn);
        bc.DestinationTableName = tableName;
        cn.Open();
        bc.WriteToServer(dt);
        cn.Close();
        MessageBox.Show("データを保存しました");
    }
}

SSMS から Excel にエクスポートするとシート毎にテーブルのバックアップが作られる。これを wb.Worksheets.FirstOrDefault で見つける。Excel からセルを読み込んで List に返す自作のコンバーター ClosedXmlRangeConverter を作っている。
List を DataTable に書き直す拡張メソッド AsDataTable を作っておいて SqlBulkCopyのWriteToServer に渡す。

ちなみに、変換元の営業業務データクラスは、EFでデータベースから自動生成させたモデルクラスをそのまま使っている。

public partial class 営業業務データ
{
    public int 業務番号 { get; set; }
    public Nullable<int> 支店コード { get; set; }
    public string 支店名 { get; set; }
    public Nullable<int> 受注番号年度 { get; set; }
    public Nullable<int> 受注番号 { get; set; }
    public Nullable<int> 受注番号枝番 { get; set; }
    public Nullable<int> 未契約番号年度 { get; set; }
    public string 未契約番号 { get; set; }
    public string 社内業務番号 { get; set; }
    public Nullable<int> 営業担当者コード { get; set; }
    public string 営業担当者名 { get; set; }
    public Nullable<int> 発注者コード { get; set; }
    public string 発注者名 { get; set; }
    public string 発注番号 { get; set; }
	...

こんな風なテーブルになっている。

?IXLCellの拡張メソッド

IXLCell 自体は GetValue() で型変換ができるのだが、今回はエンティティクラスの各プロパティの型に合わせるので、リフレクションのPropertyInfoクラスで対象の型に変換できるように拡張メソッドを作っている。

/// CloseXmlのIXLCellの拡張メソッド
public static class ClosedXmlCellExtensions
{
    public static int ToInt(this IXLCell cell)
    {
        if (cell.Value == null) return 0;
        int result = 0;
        return int.TryParse(cell.Value.ToString(), out result) ? result : 0;
    }
    public static int? ToNullableInt(this IXLCell cell)
    {
        if (cell.Value == null) return (int?)null;
        int result = 0;
        return int.TryParse(cell.Value.ToString(), out result) ? result : (int?)null;
    }
    public static double ToDouble(this IXLCell cell)
    {
        if (cell.Value == null) return 0.0;
        double result = 0.0;
        return double.TryParse(cell.Value.ToString(), out result) ? result : 0.0;
    }
    public static double? ToNullableDouble(this IXLCell cell)
    {
        if (cell.Value == null) return (double?)null;
        double result = 0.0;
        return double.TryParse(cell.Value.ToString(), out result) ? result : (double?)null;
    }
    public static decimal ToDecimal(this IXLCell cell)
    {
        if (cell.Value == null) return 0;
        decimal result = 0;
        return decimal.TryParse(cell.Value.ToString(), out result) ? result : 0;
    }
    public static decimal? ToNullableDecimal(this IXLCell cell)
    {
        if (cell.Value == null) return (decimal?)null;
        decimal result = 0;
        return decimal.TryParse(cell.Value.ToString(), out result) ? result : (decimal?)null;
    }
    public static DateTime ToDateTime(this IXLCell cell)
    {
        if (cell.Value == null) return new DateTime();
        DateTime result = new DateTime();
        return DateTime.TryParse(cell.Value.ToString(), out result) ? result : new DateTime();
    }
    public static DateTime? ToNullableDateTime(this IXLCell cell)
    {
        if (cell.Value == null) return (DateTime?)null;
        DateTime result = new DateTime();
        return DateTime.TryParse(cell.Value.ToString(), out result) ? result : (DateTime?)null;
    }
    public static bool ToBoolean(this IXLCell cell)
    {
        if (cell.Value == null) return false;
        bool result = false;
        return bool.TryParse(cell.Value.ToString(), out result) ? result : false;
    }
    public static bool? ToNullableBoolean(this IXLCell cell)
    {
        if (cell.Value == null) return (bool?)null;
        bool result = false;
        return bool.TryParse(cell.Value.ToString(), out result) ? result : (bool?)null;
    }
    public static string ToText(this IXLCell cell)
    {
        if (cell.Value == null) return "";
        return cell.Value.ToString();
    }

    public static object To(this IXLCell cell, System.Reflection.PropertyInfo pi)
    {
        var pt = pi.PropertyType;
        if (pt == typeof(int)) return cell.ToInt();
        if (pt == typeof(int?)) return cell.ToNullableInt();
        if (pt == typeof(double)) return cell.ToDouble();
        if (pt == typeof(double?)) return cell.ToNullableDouble();
        if (pt == typeof(bool)) return cell.ToBoolean();
        if (pt == typeof(bool?)) return cell.ToNullableBoolean();
        if (pt == typeof(DateTime)) return cell.ToDateTime();
        if (pt == typeof(DateTime?)) return cell.ToNullableDateTime();
        if (pt == typeof(string)) return cell.ToText();

        return null;
    }
}

コンバーター ClosedXmlRangeConverter クラス

IXLWorksheet の内容を読み込んで、目的のエンティティクラスのリストを作るためのコンバーターを作成する。シートの1行目にテーブルの列名が入っている想定で作ってある。

/// RangeからEFへのコンバーター
/// Entity Frameworkで利用するエンティティクラスに読み込む
/// 主に ClosedXmlのExcel からデータを読み込み、EF でデータベースに出力するときに使う
public class ClosedXmlRangeConverter<T> where T : class, new()
{
    protected List<System.Reflection.PropertyInfo> _Columns = new List<System.Reflection.PropertyInfo>();
    protected IXLWorksheet _sh;
    /// コンバーターの作成
    public ClosedXmlRangeConverter(IXLWorksheet sh)
    {
        _sh = sh;
        // 最初の行をコンバート先のテーブルと照合する
        var props = typeof(T).GetProperties();
        int col = 1;
        while (sh.Cell(1, col).Value.ToString() != "")
        {
            var text = sh.Cell(1, col).Value.ToString();
            var prop = props.FirstOrDefault(t => t.Name == text);
            if (prop != null)
            {
                _Columns.Add(prop);
            }
            col++;
        }
    }

    /// 行単位でコンバート
    public T ToItem(int row)
    {
        var item = new T();
        for (int col = 0; col < this._Columns.Count; col++)
        {
            var prop = _Columns[col];
            var o = _sh.Cell(row, col + 1).To(prop);
            prop.SetValue(item, o);
        }
        return item;
    }
    /// 全てのデータをコンバート
    public List<T> ToList()
    {
        var items = new List<T>();
        int r = 2;
        while (_sh.Cell(r, 1).GetValue<string>() != "")
        {
            var item = this.ToItem(r);
            items.Add(item);
            r++;
        }
        return items;
    }
}

エンティティクラスをDataTableに変換する拡張メソッド

DbSet から DataTable を作成するための AsDataTable() を作る。そのままでは、DataRow の型に Nullable が入らないので、DBNull への切り替えを行っている。

/// 
<summary>
/// DbSetをDataTableに変換
/// </summary>

public static class DataTableExtenstions
{
    public static DataTable AsDataTable<T>(this DbSet<T> src) where T : class
    {
        return DataTableExtenstions.AsDataTable(src.Local);
    }
    public static DataTable AsDataTable<T>(this IEnumerable<T> src) where T : class
    {
        var properties = typeof(T).GetProperties();
        var dest = new DataTable();
        // テーブルレイアウトの作成
        foreach (var prop in properties)
        {
            if ( prop.PropertyType.IsGenericType == true &amp;&amp;
                    prop.PropertyType.GetGenericTypeDefinition().Name == "Nullable`1")
            {
                /// Nullable<int>のときは、
                /// DataRowの中身を DBNull と int の時に分けなければいけない。
                var originalType = prop.PropertyType.GetProperty("Value").PropertyType;
                var column = new DataColumn();
                column.DataType = originalType;
                column.AllowDBNull = true;
                column.ColumnName = prop.Name;
                dest.Columns.Add(column);
            }
            else
            {
                dest.Columns.Add(prop.Name, prop.PropertyType);
            }
        }
        // 値の投げ込み
        foreach (var item in src)
        {
            var row = dest.NewRow();
            foreach (var prop in properties)
            {
                var itemValue = prop.GetValue(item, new object[] { });
                row[prop.Name] = itemValue ?? System.DBNull.Value;
            }
            dest.Rows.Add(row);
        }
        return dest;
    }
}

計測はあとで

これで Excel シートから SQL Server への書き戻しが高速に行える。全てのデータを書きこ戻してしまうので、ピンポイントで修正することはできないがマスターテーブルの書き換えとかを Excel 上で行って、SQL Server に戻すことができれば結構便利だと思う。修正自体もお客さんに行って貰うこともできそうだし。

スピードは、COM 経由よりも100倍位早いのだが、これは後で実測してみる。

カテゴリー: 開発 | ClosedXMLを使って、超高速にExcelからSQL Serverへデータ転送する はコメントを受け付けていません

LINQ の INSERT を SqlBulkCopy にするとどれだけ早くなるのか?

昨日書いたばかりの、これだけど、SqlBulkCopy を使うとどれだけ早くなるのかを再び実験

LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする
http://www.moonmile.net/blog/archives/9646

結論

結論から言えば、SqlBulkCopy を使うほうが100倍位早いですね。1万件位だと6秒から0.06秒という誤差?っぽい感じだけど、100万件になるとLINQのINSERTでは難しいので、SqlBulkCopy を直接使えという感じです。

SqlBulkCopy は DataTable を受け取る

SqlBulkCopy Class (System.Data.SqlClient) | Microsoft Docs
https://docs.microsoft.com/ja-jp/dotnet/api/system.data.sqlclient.sqlbulkcopy?view=netframework-4.7.2

SQL Server専用の SqlBulkCopy な訳ですが、引数に DataTable か DataRow の配列を取ります。いわゆるEFじゃない DataSet/DataTable のものを使わないといけないので、EFのDbSetがそのまま渡せません。
逆に言えば、EFのDbSetを渡せるようにDataTableに変換してやれば、LINQとSqlBulkCopyが共存可能になります。

任意のオブジェクトの配列をDataTableに変換する – Qiita
https://qiita.com/keidrumfreak/items/f092b3cacfc2961610b6

この記事を参考にしながら、というかそのまま使って、AsDataTable という拡張メソッドを作ります。

public static class DataTableExtenstions
{
    public static DataTable AsDataTable<T>(this DbSet<T> src) where T : class
    {
        return DataTableExtenstions.AsDataTable( src.Local );
    }
    public static DataTable AsDataTable<T>(this IEnumerable<T> src) where T : class
    {
        var properties = typeof(T).GetProperties();
        var dest = new DataTable();
        // テーブルレイアウトの作成
        foreach (var prop in properties)
        {
            dest.Columns.Add(prop.Name, prop.PropertyType);
        }
        // 値の投げ込み
        foreach (var item in src)
        {
            var row = dest.NewRow();
            foreach (var prop in properties)
            {
                var itemValue = prop.GetValue(item, new object[] { });
                row[prop.Name] = itemValue;
            }
            dest.Rows.Add(row);
        }
        return dest;
    }
}

EFのDbSetは内部でLocalプロパティ(データベースに反映する前のデータを取っている持っておくコレクション)があるので、これを利用して DataTable に変換します。

実験

SqlBulkCopy に渡すデータを List で用意してから DataTable に変換するパターン

private void clickBulk(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand("delete BulkT");
    var start = DateTime.Now;
    var lst = new List<BulkT>();
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString("N"),
            Created = DateTime.Now,
        };
        lst.Add(t);
    }
    var dt = lst.AsDataTable();
    var cn = ent.Database.Connection as SqlConnection;
    var bc = new SqlBulkCopy(cn);
    bc.DestinationTableName = "BulkT";
    cn.Open();
    bc.WriteToServer(dt);
    cn.Close();

    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

SqlBulkCopy に渡すデータをLINQのAddで追加してから DataTable に変換するパターン

private void clickAsDataTable(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Configuration.AutoDetectChangesEnabled = false;
    ent.Configuration.ValidateOnSaveEnabled = false;
    ent.Database.ExecuteSqlCommand("delete BulkT");
    var start = DateTime.Now;
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString("N"),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
    }
    var cn = ent.Database.Connection as SqlConnection;
    var bc = new SqlBulkCopy(cn);
    bc.DestinationTableName = "BulkT";
    var dt = ent.BulkT.AsDataTable();
    cn.Open();
    bc.WriteToServer(dt);
    cn.Close();

    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

結果

List を使って DataTable に変換
0.0477976
0.0400377
0.034159

LINQのAddを使って DataTable に変換
1.7919409
1.7825315
1.7763977

AutoDetectChangesEnable = false の場合
6.311641
5.8724151
6.0832671

単純に比較すると、AutoDetectChangesEnable を false にしただけよりも、SqlBulkCopy を使ったほうが6倍位早くなります。さらに、EFのDbSetを使わずに、Listだけを使った場合は、25倍位早くなるってことですね。どうやら、DbSetに対してAdd/Removeしたときに DetectChanges() 等のチェックルーチンが走るらしく、単純な大量 INSERT の場合には SqlBulkCopy を直接使ってしまたほうが早いです。

カテゴリー: 開発, C# | LINQ の INSERT を SqlBulkCopy にするとどれだけ早くなるのか? はコメントを受け付けていません

LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする

とあるシステムで1万件程度のテーブルを構成しなおして別の複数のテーブルに移すことをやっていた。いわゆる、正規化していないテーブルをデータ移行の際に正規化しようと思って、複数のテーブルに分けたのだが、たかだか1万件しかないのに非常に遅い。正規化のロジックが遅いのかもしれないけど、1万件程度をデータ挿入するのに30分位掛かってしまうのである。
相手が SQL Server なので SqlBulkCopy を使えば結構なスピードになるはずなのだが、100万件の場合ならばそうかもしれないけど、たかだか1万件の挿入でこんなに遅いのは変。ということで、LINQ の INSERT について調べなおしてみる。

結論

結論から言えば、AutoDetectChangesEnabled と ValidateOnSaveEnabled を OFF(false) にすればよい。

DbContextConfiguration.AutoDetectChangesEnabled Property
https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.infrastructure.dbcontextconfiguration.autodetectchangesenabled?redirectedfrom=MSDN&view=entity-framework-6.2.0#overloads

ent.Configuration.AutoDetectChangesEnabled = false;
ent.Configuration.ValidateOnSaveEnabled = false;

LINQ で INSERT/DELETE/UPDATE をする場合、EF の内部で整合性をチェックしている。System.Data.Entity.DbSet.Add などを呼び出したときに、DetectChanges() でチェックをしているというわけだ。どうやらこれが遅い原因なので、素直に System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled の値を false にして呼び出さないようにすればよい。
デフォルトでは、AutoDetectChangesEnabled が true となっている。

実験

CREATE TABLE [dbo].[BulkT](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[GUID] [varchar](100) NOT NULL,
	[Created] [datetime] NOT NULL
)

この BulkT テーブルに1万件のデータを挿入する。SaveChanges を1回の挿入ごとに行っているが、テーブルが複雑な場合は最後に1回だけだとメモリを食いすぎたりするため、何度かに分ける(100回毎など)必要がある。

private void clickNormal(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand("delete BulkT");
    var start = DateTime.Now;
    for ( int i=0; i<10000; i++ )
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString("N"),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
        ent.SaveChanges();
    }
    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

private void clickNoAutoDetect(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand("delete BulkT");
    var start = DateTime.Now;
    ent.Configuration.AutoDetectChangesEnabled = false;
    ent.Configuration.ValidateOnSaveEnabled = false;
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString("N"),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
        ent.SaveChanges();
    }
    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

DetectChanges のチェックあり(AutoDetectChangesEnabled = true)
50.1799167
48.9856614
48.5278795

DetectChanges のチェック無し(AutoDetectChangesEnabled = false)
6.1251368
6.1039912
6.2945836

このように8倍ぐらいの差がでてくる。100件程度ならば特に問題もでないだろうが、1万件以上ある場合は気を付けたおいたほうがよいだろう。データチェックが入らないので、挿入データに気を付ける必要があるが、今回のように空のテーブルに挿入する場合はプログラム内でチェックが済んでいるので特に問題はない。

ちなみに、30分以上掛かっていたデータコンバートは1分以内に終わるようになった。

参考リンク

Entity Framework のパフォーマンス #2 更新処理 | C#.NET vs VB.NET
http://csharpvbcomparer.blogspot.com/2015/04/net-ef-performance-2-updating.html

カテゴリー: 開発, C# | LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする はコメントを受け付けていません

もっと簡単に超概算見積もりver2

以前、超概算見積もり を考えたのはすでに10年前になるので、この際だからバージョンアップ版をあげておこう。この超概算見積もりの手法は PMBOK で言うところの超概算見積もりとは違う。だが、おそらく現実に即しているはずだ。

ざっくり規模見積もりをしたいときに、機能から見積もり始めるのは間違いだ。特に受託開発の場合には、営業さんの云うところの「ざっくり見積もる」というのは、予算と期間になる。いわゆる、QCD で言うところのコストと期限(Delivery)になる。

このため、各種の見積もり手法では「規模見積もり」→「予算見積もり」→「スケジュール」の順番で見積もりをしていくのだが、実際のところ、

  • 予算が限られている(Cost)
  • リリース日などの期限が決まっている(Delivery)

ことが多いので、Cost と Delivery の制約を先に埋めてしまうほうがよい。

予算(コスト)が決まっている場合

お客や営業さんから「ざっくり予算はいくらでしょうか?」と聞かれることが多いだろうが、言っている人の中にはすでに「ざっくりとした予算」があることが多い。だから、「ざっくりとした予算は?」と聞かれたならば、「あれとこれとこれを組み合わせたら、最大で1億円ですね」と答えるとよい。当然「1億円なんて予算はないよ」という顔をするから(億円単位の大規模開発ならば別だろうけど)、「あれとこれを削って、ここだけ作れば100万円ですかね」と、あきらかに機能不足な話の低予算を提示するとよい。そうすると「そんなに機能が少ないんじゃあだめで、これとこれぐらいはないと」という回答が得られる。

つまり、最大限と最小限の間に「予算」は決まっているのは確かなことなのだ。

その間の部分で、仮決めで(相手の懐を予想しながら、あるいは直接「予算はどれくらいでしょうか?」と聞いても良い)、500万円ぐらいの予算で、と決めたとしよう。

人月計算をやりやすくするため単価100万円/月のソフトウェア開発者を割り当てるとすると、単純計算する超概算見積もりで「予算は500万円、開発期間は5か月」というプロジェクトが生まれる。開発期間は5か月が上限なので、これを超えるとプロジェクトは赤字だ。赤字なプロジェクトは受けてはいけない案件である(時と場合によるけど)。

これを自社で行っているソフトウェア開発手法で見積もってみる。

  • スクラムのスプリントを使った場合、スプリントが2週間であれば10スプリント
  • チケット駆動で、3チケット/日とすれば、3x20x5 = 300 チケット
  • WBS やタスクで週平均3程度を想定すれば、20/3 x 5 = 33 WBS

このように スプリント/チケット/WBS/タスク の上限が決まってくる。

チケットの内容をすべて埋めなくても良い。ある程度チケットの内容を埋めてしまえば、

  • 予算以内におさまりそうか(チケットが枯渇しなさそうか)
  • 予算以上になりそうか(チケットが大幅に上回るだろうか)

ということがわかる。超過する場合には、500万円という予算を優先して(お客がそれしか出せないのだから仕方がない)、

  • 機能(チケット)を減らす → 何か盛り込みすぎている
  • 単価を下げる → 並行プロジェクトにより、スケジュールに余裕を持たせる。薄く引き伸ばす

ことになる。うすく引き伸ばす方法は、並行プロジェクトを使ったリスク分散の方法だ。これはまた別の機会に話す。

スケジュール(期限)が決まっている場合

年度末までに開発を終えるとか、元号対応だとか別の理由でスケジュールが決まっている場合がある。このときも「ざっくりと予算と期間はどのくらいですか?」と聞かれるのだが、期限は後ろに倒せない。

例えば、半年後にリリースが決まっている6か月のプロジェクトを想定しよう。すると、単価100万円/月の場合は、600万円の予算となる。ここからチケット数を決めて、概算で割り振ってみる方法は、予算優先の場合と同じだ。

期限内におさまりそうにない(必要なチケットの数が予想よりも多い)場合は、

  • 人数を増やす → 単純に馬力を増加させる。当然、予算は増加する。
  • 機能を減らす → 期限優先なので、優先度が低い機能は落としてしまう。

リリース日を後ろ倒しにできないので、保険(2割増しあるいは5割増し)を入れておいてリリース日に確実に間に合うようにすることを忘れずに。

予算(コスト)や期限(スケジュール)で概算する

最初から機能(FP法やCOCOMO2など)を使って予算や期間を見積もりしてしまと、結局のところお客が想定している予算や期限と食い違いがでてしまって「機能」自体の見積もりしなおしになる。だから、予算や期限から機能の方を逆算して、規模見積もりが分かったところから再び予算や期間を見積もり直せばよい。特に、客相手の受託開発の場合にはここがポイントになる。

プロジェクト実行時の再見積もり

超概算見積もりをした後に再チェックをして、予算や期間ができたとしよう。このときの見積もりの根拠は「機能」がベースになっているので、プロジェクト実行時にこの機能(規模見積もり)が正しいかどうかを再チェックする。

さきに書いた通り、受託開発では予算と期限が優先なのだから、プロジェクト実行時に注目するポイントは以下になる。

  • 機能が増加していないか?
    → チケットが増えていないか?規模見積もりの前提条件がずれていないかをチェックする。
  • チケットの消化数は適切か?
    → バーンダウンチャートでチケットを消化する傾きをチェックする。
    → 期限に間に合うか?
  • 並行プロジェクトがある場合は、適切にチケットが消化されていないか?
    → 他方のプロジェクトに押されていないか?
    → 仕事の時間が増えていないか。生産効率が落ちていないか?

チケットの消化数を守るために、残業や休出をして補っている場合、勤務時間が増えているのだから「生産効率が落ちている」状態になる。生産量(この場合は消化するチケット数)は同じで、時間が掛かりすぎているのだから、仕事の効率が落ちているとみる。

これらをプロジェクト実行時にはチェックして、対処(人を増やす、予算を増やす、期限をずらす、機能を減らす)を行うのがプロジェクトマネジメントである。

計測のための数え上げに関しては、デマルコ氏の進捗管理やマコネル氏のWBS等の数え上げによる見積もり手法を参考にするとよい。

カテゴリー: OpenCCPM | もっと簡単に超概算見積もりver2 はコメントを受け付けていません

プロジェクトマネジメントの工学的アプローチのメモ書き

ソフトウェア開発におけるプロジェクトマネジメント手法を工学的にアプローチするメモを流しておこう。ちなみに、この記事には「モチベーション」という言葉は出てこないし「スーパープログラマ」も「カリスママネージャー」も出てこない。あくまで工学/自然科学的なアプローチで解決をしようという話である。ある意味「働き方改革」も出てこない。

前提条件

「プロジェクト」とは何かというところからスタートするほどでもないが、この手法には前提条件がある。

  • WBS/チケット/タスクが全て消化されれば、プロジェクトが終了する

という条件が必要になる。一見当たり前のように見えるけど、研究プロジェクトや目的達成型のプロジェクトの場合にはこのマネジメント手法では無理がある。研究プロジェクトは数々の試行錯誤が必要なので、チケットに対する時間効果が判別つかない。また一定の目的を達成する(製品販売数とかユーザー数目標とか)プロジェクトの場合も、達成するまでにいろいろな試行錯誤が出てくるので、形式的なプロジェクトマネジメント手法は向かない。この場合は、(おそらく)イノベーションマネジメントになると思う。こっちの方はまた別の機会に書くことにする。

WBSあるいはチケットあるいはタスクは、何らかの仕事の「粒」となる粒度ともいう。面倒なので以後「チケット」で用語を統一するが、それぞれの違いは

  • PMBOK的にトップダウンで仕事を出す場合は「WBS」
  • ボトムアップ的に仕事を出す場合は「タスク」
  • チケット駆動の場合は「チケット」

ということになるが、ひとまとめにして「チケット」と呼ぶ。
このチケットの数や大きさ(作業期間や外注金額など)などは、プロジェクト開始以降から変化してもよいものとする。PMBOKの場合は事前にWBSを出し切る要請があったりするが、ここでは後からWBSを追加可能とする。タスクの場合も同じ。あとから追加要望とか見落としとかでタスクが増える場合も考えられる。

主に受託案件を考える

適用範囲は主に受託案件を考える。社内製品開発でもよいのだが、社内の場合はもう少し制約が緩い場合が多いので、厳しいほうで試してみるのがよいだろう。受託案件の場合、顧客から予算と期間が区切られることが多く、単純なアジャイル開発による「交渉」機能がうまく働かないことが場合が多い。大幅に機能が増加されていれば予算枠を増やす交渉をすることも可能なのだが、ちょっとした機能追加とか受託側の都合による機能追加/見落としなどでの「仕事」の追加では追加予算が出ないのが普通だ。よって、

「チケット」が増えたにも関わらず、「予算」や「納期」が動かないことが多い。

むしろこちら側であるはずの「営業サイド」から値下げ要求や期間短縮、コストダウンなどを要求される場合が多いというのが受託開発でのソフトウェア開発での特徴だ。
この場合「マネージャー」もあちら側に回ることもできるのだが、今回はこちら側に回ることにする。あちら側にまわるやり方は色々出ているので試してみるといい。お勧めはしないが。

これにより、受託開発で案件を受けるときの条件として

  • 開発期間、納期が決まっている。

→ よって、無理なスケジュールを強制されたときは、最初から拒否する。

  • 予算の上限がある。

→ よって、無理な予算を強制されたときは、最初から拒否する。

  • 開発すべき機能が決まっている。

→ よって、無理な機能追加/拡張が要求されたときは、最初から拒否する。

ということにする。「無理な」というのは、あきらかに開発期間が短かったり、低予算すぎたり、夢のような機能がてんこもり、という場合のことだ。最初から失敗するプロジェクトから逃げるというのは有能なマネージャやリーダーの鉄則だ。もちろん、あちら側に回ればそういう案件を受けることも可能であるのだが、それは(以下略

さて、達成できそうな受託案件が目の前にできたとしよう。「達成できそうな受託案件」というのは、ほどよく顧客からの要望がでてきて、QCD(機能、コスト、納期)の概算ができたときに限るという訳だ。ここで問題になるところだが、これは2つの問題を内包している。

  • 受託開発案件における妥当なQCDをどうやって出すのか?
  • 妥当なQCDの開発プロジェクトを「成功」させるためには、どうしたらよいのか?

まるで、鶏と卵の問題のようだが、この2つは同時に解決する。というか同時に解決しないといけない。

計画と工程管理を一度に検証する

具体的な手順を示そう。

1.初期の条件として「無理のない」QCD を考える。ここは初期値なので、勘で適当に決めて良い。あるいは、顧客の状態を考えて、ざっくりと予算と納期(開発期間)を決めてしまうのがよい。よく営業さんが言う「ざっくりと」で良い。

2.社内で開発するチームの「チケット」の消費スピードを決める。理想は1チケット2,3時間程度なのだが、1チケットが1日でもよい。1日以上のチケットを考えるとプロジェクト実行時に誤差が多くなるので、1日以内におさめたほうがよい。

例えば、1日3チケット、期間が半年、メンバが5人と考えれば、
3x20x6x5=1800チケット
ということになる。

3.総チケットのうち、20%から30%ぐらいが「予備」のチケットとなる。予備というのは不確定要素を吸収するためのチケットで、プロジェクトを進行したときの変更要求とか機能が膨らんだときの仕事を吸収するためのチケットである。20%から30%ってのは経験上のもので、これはよく言われる「プロジェクトの予算を1.2倍から1.5倍するとよい」と同じことになる。いわゆる逆数だ。0.2/1.2 ≒ 17%、0.5/1.5 ≒ 33% となる。

残りの仕事のためのチケットを使って1で見積もったときの機能実装などを割り振る。WBS的にトップダウンで割り振ってもよいし、ボトムアップ的にタスクとして割り振ってもよい。それぞれの期間が揃っているほうがいいのだが、あまり気にしなくてもよい。重要なのは最初から割り振られているチケットの数になる。最初から割り振るチケットは「必ずやらなければいけない仕事」なので、優先度が高い。
ここでチケットが足りなくなったり、予備チケットを使い始めたらダメだ。最初の1の「ざっくり開発期間」が間違っていることになる。ざっくりの部分をざっくりと直して、2のチケット数を増やしていく。

逆に、すべての仕事チケットを埋める必要はない。概算の概算として設計工程/実装工程/試験工程で3つの山に分けてしまって、設計工程のチケットだけ書いてみるという方法でもよい。空白チケットがあってもよい。

計画段階ではこれでおしまい。ここで、おおむねチケット駆動のシミュレーションがおわってる。

  • トータルのチケット数(予備を含む)
  • 開発期間
  • 人数

が分かったので、1日(あるいは1週間)で消化するチケット数が計算できる。これが「生産量」になる。

4.プロジェクトが進行している間は順次チケットを消化する。チケット駆動のツールを使ってもよいし、ひとりプロジェクトならば Excel だけでもよい。
仕事チケットを日々消化すると同時に、予期しない仕事があれば予備チケットを使う。顧客からの要望が増えれば予備チケットを使う。営業からの要求があれば予備チケットを使う。増えたら予備チケットを使う。

仕事チケットが大幅に膨らんでしまった場合は、複数の予備チケットに分割する。当然、予備チケットではない空白チケットがあればそれを使う。考慮不足を補うのが空白チケットや予備チケットの役目だ。空白チケットはもともと予定されているけど内容が書かれなかったもの(面倒だったとか、書く時間がなかったとか)、予備チケットはいわゆる「プロジェクトバッファ」分のチケットのことになる。

5.進捗管理(工程管理)は、実際のチケット消化数と3で計算した事前のチケット消化状態と比較する。順調であれば傾き(バーンダウンチャート)は同じぐらいになる。実際の消化数が多い場合は特に問題はない。ちょっとぐらい遅れていても問題はない。総チケットの上に行かなければよい、かつバーンダウンチャートの予想直線を引いたときに総チケットのラインよりも傾きが浅くならなければよい。

予備チケットの分だけ保険が入っているので、進捗はこの間をうろうろすることになる。
バーンダウンチャートを引いたときに、赤い線より上に出そうであれば、プロジェクト進行を見直すことになる。
「生産量」を上げることはできないので(突如として「生産量」があがるという幻想は捨て去るべきだ)、

  • プロジェクトメンバを増やす
  • 仕事チケットを減らす(開発機能を減らす)
  • 仕事をする時間を増やす=休日出勤

ことになる。当然、開発予算を増やす交渉や納期を後ろに倒す交渉もし始める。休日出勤は緊急避難的なのでカンフル剤としてか使えない。一瞬しか進捗は良くならない。受託案件の場合、予算や納期が変わらないことが多いので、先の2点が対処になるだろう。

以上の5ステップで、計画と進捗管理はおしまい。

利点

このプロジェクト管理での利点は、先に書いた通りメンバのモチベーションとかやる気とかコミュニケーションとかの不確定要素が一切出てこない、それに頼らないことだ。それらはチーム開発では必要かもしれないが、必ずしも高度なものである必要はない。一般的にできれば十分だ。少なくとも、受託案件のような場合には「標準的な生産量」の測定が重要になるので、不確定要素に頼らないほうがよい。

欠点

これは利点と同時に欠点でもあるのだが、この方式にはミクロな視点が入らない。チケット同士の関連やなんらかの要因による「生産量」(チケットの消化スピード)の低下を予測できない。スケジュール管理自体がメンバ任せになってしまうので、効率の悪いチケットの消化をしてしまう可能性がある。

当然のことながら、規模が大きくなる場合は、

  • チケット同士の関連性(PERT図)
  • マイルストーン(ガントチャート)

が必要になってくる。この2つの図はおいおい活用していくとして、ガントチャートは「チケットの増加に素早く対応できない」という大きな欠点がある。チケットやタスク、WBSを増やしたときにガントチャートを修正するのが難しいため「ガントチャートが直せないから、チケットを増やさずにガントチャートのスケジュール通りに行動する」という本末転倒な心理が働いてしまう。これでは駄目だ。これはツールによる制約があるので OpenCCPM で解消していきたい点である。

おわりに

メモなのでまとめはいらないのだが、作業見積もりの20%増しとか50%増しとかで顧客に提出するとき、その保険が削られて営業サイドで勝手に値引きされてしまうことがある。お客の前で「値引き」すると案件が取りやすいからね。営業テクニックである。仕方がない。なので、見積もりを出す側(マネージャでもリーダーでも誰でも良い)では営業の値引きが明らかになっている場合は、あらかじめ「値引き」の値段だけ予算に加えておけばよい。値引き自体に根拠はないのだから。

また、予備チケットや保険の分が「顧客」に理解されないときは、計画段階や進捗管理で「二重帳簿」を作るとよい。二重帳簿は Excel などを使って自動計算すると簡単に作れる。

  • 顧客や営業にとって安心されやすい「進捗」グラフ
  • プロジェクトメンバがリスク管理できる「進捗」グラフ

を二種類用意するわけだ。超概算見積もりも同じパターンで作ることができる。その話は別の機会に。

カテゴリー: 開発, OpenCCPM | プロジェクトマネジメントの工学的アプローチのメモ書き はコメントを受け付けていません

読み取り専用の値オブジェクトがWPFのTextBoxにバインドされない(ように見える)件

最終的に、WPFのバグを踏んだのか!?と思ったけど、よく見ればきちんと例外が出ていたという話なので、今後注意するという備忘録的な記事です。

現象

こんな風な値オブジェクトを作っておいて、WPFのウィンドウにバインドします。

public class ViewModel
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string NameSan { get => Name + "-san"; } 
}

NameSan プロパティは加工して表示するだけの読み取り専用のプロパティです。金額の合計値を出すとか、なにか計算結果をだすとかそういう ReadOnly な表示はよくやるパターンですね。

ViewModel _vm;
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    _vm = new ViewModel();
    _vm.ID = 100;
    _vm.Name = "Masuda";
    this.DataContext = _vm;
}

ここで NameSan プロパティを表示するときに、TextBlock タグを使えばよいのですが、TextBox を使って ReadOnly=”True” にします。TextBox を使う理由としては、表示している文字列のコピーが Ctrl-C でできるからです。けれど、読み取りにしたいから、ReadOnly を付けておきます。

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding ID}" />
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Name}" />
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding NameSan}" IsReadOnly="True" />

これをビルドして動かそうとすると実行時に例外が発生します。

System.InvalidOperationException
HResult=0x80131509
Message=TwoWay または OneWayToSource バインドは、型 ‘WpfApp4.ViewModel’ の読み取り専用プロパティ ‘NameSan’ では動作できません。
Source=PresentationFramework

どうやら、TextBox の Text プロパティは読み書き同時にできることが前提となっているので NameSan のように get しかない読み取り専用プロパティではエラーが発生するのです。IsReadOnly を付けていても駄目ですね。これは、WPFのModeがデフォルトで「TwoWay」になっており双方向になっているのが原因です。ここを明示的に「Mode=OneWay」にして表示のみにすると実行時エラーはでなくなります。

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding ID}" />
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Name}" />
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding NameSan, Mode=OneWay}" IsReadOnly="True" />

ちなみに、UWPの場合はデフォルトが「OneWay」になっているので動作が違います。これがややこしい。

実験

あらかじめ、この現象を知っておけば Mode=OneWay をちまちまつけるのですが、どうやら手元でたくさんサブウィンドウを作っているときにうっかり OneWay をつけ忘れたのです。

すると、サブ画面ではこんな風に NameSan プロパティをバインドしている TextBox の初期値がでなくなります。

サブ画面を開くときは、こんな風に ViewModel を渡して(本当はコンストラクタのほうがいいけど)、ShowDialogメソッドで開きます。

private void clickOpen(object sender, RoutedEventArgs e)
{
    var sub = new SubWindow();
    sub.VM = _vm;
    sub.ShowDialog();
}

実行に落ちることはなく、単にバインドに失敗します。

これ、実行時の出力を注意深くみると、実は「例外」が発生しています。

メイン画面のときに失敗したとき同じように、バインド時に例外が発生しているのですが、ShowDialogメソッド内で例外を握りつぶしているので、初期値が表示できない(バインドできない)現象だけが出てサブウィンドウ自体は表示されるという現象です。

所感

EF で作った自動生成のオブジェクトは get/set が揃っているので、この現象は発生しません。LINQ で読み込んだリストとかをグリッドにバインドするとか TextBox にバインドしても get/set があるので助かるのだけど。
計算結果の表示とか、なんらかの文字列を加工した後に表示するときに get だけの読み取り専用プロパティを作ったときにはまります。しかも、これ TextBlock の場合はもともと読み取り専用(表示専用)なので代用部なんだけど、TextBox のような読み書き用のコントールだけに当てはまる現象だという、結構レアなケースですが、覚えておくとハマりにくい穴かもしれません。

カテゴリー: 開発, C#, WPF | 読み取り専用の値オブジェクトがWPFのTextBoxにバインドされない(ように見える)件 はコメントを受け付けていません