20190506のC#に関する記事は6件です。

R, Python, Juliaの性能比較

はじめに

結果

評価関数の呼び出し回数を考慮すると、Juliaが最速で、その後にC++、Python、R、C#と続く。

言語環境 秒数 呼び出し回数 平均秒数 比率
Julia(1.1.0) 10.86 70 0.155 1
C++(VS2017) 8.51 47 0.181 1.17
Python(3.6.7/WSL) 13.06 59 0.221 1.43
R(3.5.3) 38.6 47 0.822 5.3
C#(VS2017) 47.6 47 1.013 6.5

Pythonで測定したところ、CPUの利用率が100%となり、24秒くらいかかった。
OMP_NUM_THREADS=1として使用するスレッド数を1に制限したら速くなった。
C#が遅すぎるが、Install-Package MathNet.Numerics.MKL.Win-x64として、
Intel Math Kernel Library (MKL)を使用すると劇的に速くなる。

言語環境 秒数 呼び出し回数 平均秒数 比率
C+++MKL 4.58 47 0.097 0.63
C#+MKL 6.59 47 0.140 0.90
R Open(3.5.2)+MKL 7.51 47 0.160 1.03
Python(3.7.3)+MKL 10.9 59 0.184 1.19

Microsoft R Openは、デフォルトでMKLを使用する。使用スレッド数を1に制限。
MinicondaでPythonをインストールするとnumpy+MKLを使用するため速くなる。
最速はC++/Eigen/MKL。
Julia+MKLは未評価。MKLを使用するためにはソースからコンパイルする必要あり。

処理の大部分は行列(767x767)のコレスキー分解であり、MKLの効果が大きい。

サンプルデータ

USPS handwritten digit data
http://www.gaussianprocess.org/gpml/data/

R

## データの入力

library("R.matlab")

mat_data <- readMat('usps_resampled.mat')

index3 = (mat_data$train.labels[3+1,] == 1)
index5 = (mat_data$train.labels[5+1,] == 1)

y_train = ifelse(index3,1,-1)[index3 | index5]
x_train = mat_data$train.patterns[,index3 | index5]

## カーネル関数とシグモイド関数

x_train_L2 = apply(x_train,2,function(a){colSums((x_train-a)^2)})
my_kernel_train = function(k_sigma,k_scale){
  exp(x_train_L2 / (-2 * k_scale^2)) * k_sigma^2
}

my_sigmoid = function(y,f){
  pnorm(y * f)
}
my_sigmoid_log_1st = function(y,f,prob){
  y * dnorm(f) / prob
}
my_sigmoid_log_2nd = function(y,f,prob){
  a = dnorm(f) / prob
  a * (a + y * f)
}

## 周辺尤度の最大化

my_evidence_optim = function(par){
  k_scale = exp(par[1])
  k_sigma = exp(par[2])
  K11 = my_kernel_train(k_sigma,k_scale)
  n = length(y_train)
  f = seq(from=0,to=1,length.out=n)
  while(TRUE){
    prob = my_sigmoid(y_train,f)
    V1 = my_sigmoid_log_1st(y_train,f,prob)
    V2 = my_sigmoid_log_2nd(y_train,f,prob)
    V2q = sqrt(pmax(V2,0))
    B = diag(n) + t(K11 * V2q) * V2q
    U = chol(B) # t(U) %*% U
    b = V2 * f + V1
    a = b - backsolve(U,forwardsolve(t(U),(K11 %*% b) * V2q)) * V2q
    f0 = f
    f = drop(K11 %*% a)
    if(max(abs(f - f0))<1e-5)break
  }
  -sum(a * f) / 2 + sum(log(prob)) - sum(log(diag(U)))
}
start = proc.time()
ret_optim = optim(c(2.85,2.35),my_evidence_optim,control=list(fnscale=-1))
proc.time() - start
ret_optim

Julia

Rとは異なり、whileでも変数のスコープが構成される。
sum()したら次元をドロップして欲しい。

using MAT
using Distributions
using Optim
using LinearAlgebra

## データの入力

matfile = matopen("usps_resampled.mat")

train_labels = read(matfile, "train_labels")
train_patterns = read(matfile, "train_patterns")

index3 = (train_labels[3+1,:] .== 1)
index5 = (train_labels[5+1,:] .== 1)

y_train = ifelse.(index3,1,-1)[index3 .| index5]
x_train = train_patterns[:,index3 .| index5]

## カーネル関数とシグモイド関数

x_train_L2 = mapslices(t -> dropdims(sum((x_train .- t) .^ 2, dims=1), dims=1), x_train, dims=1)
function my_kernel_train(k_sigma,k_scale)
  exp.(x_train_L2 / (-2 * k_scale^2)) * k_sigma^2
end

function my_sigmoid(y,f)
  cdf.(Normal(), y .* f)
end
function my_sigmoid_log_1st(y,f,prob)
  y .* pdf.(Normal(), f) ./ prob
end
function my_sigmoid_log_2nd(y,f,prob)
  a = pdf.(Normal(), f) ./ prob
  a .* (a .+ y .* f)
end

## 周辺尤度の最大化

function my_evidence_optim(par)
  k_scale = exp(par[1])
  k_sigma = exp(par[2])
  K11 = my_kernel_train(k_sigma,k_scale)
  n = length(y_train)
  f = Array(range(0,1,length=n))
  evidence = 0
  while true
    prob = my_sigmoid(y_train,f)
    V1 = my_sigmoid_log_1st(y_train,f,prob)
    V2 = my_sigmoid_log_2nd(y_train,f,prob)
    V2q = sqrt.(max.(V2,0))
    B = I + K11 .* V2q .* V2q'
    C = cholesky(Symmetric(B))
    b = V2 .* f + V1
    a = b - (C.U \ (C.L \ ((K11 * b) .* V2q))) .* V2q
    f0 = f
    f = K11 * a
    if maximum(abs.(f - f0)) < 1e-5
      evidence = -dot(a,f) / 2 + sum(log.(prob)) - log(det(C)) / 2
      break
    end
  end
  evidence
end

@time result = maximize(my_evidence_optim, [2.85,2.35], NelderMead())

Python

np.arraynp.matrixの二種類があり挙動が異なる。

import os
os.environ["OMP_NUM_THREADS"] = "1"

import time
import numpy as np
from scipy import io
from scipy.stats import norm
from scipy.linalg import cholesky
from scipy.linalg import solve_triangular
from scipy.optimize import minimize

## データの入力

matdata = io.loadmat('usps_resampled.mat')

train_labels = matdata["train_labels"]
train_patterns = matdata["train_patterns"]

index3 = (train_labels[3,:]==1)
index5 = (train_labels[5,:]==1)

y_train = np.where(index3,1,-1)[np.logical_or(index3,index5)]
x_train = train_patterns[:,np.logical_or(index3,index5)]

## カーネル関数とシグモイド関数

x_train_L2 = np.apply_along_axis(lambda x: np.sum(np.power(x_train-x.reshape(-1,1),2),0),0,x_train)
def my_kernel_train(k_sigma,k_scale):
  return np.exp(x_train_L2 / (-2 * k_scale * k_scale)) * (k_sigma * k_sigma)

def my_sigmoid(y,f):
  return norm.cdf(y * f)

def my_sigmoid_log_1st(y,f,prob):
  return y * norm.pdf(f) / prob

def my_sigmoid_log_2nd(y,f,prob):
  a = norm.pdf(f) / prob
  return a * (a + y * f)

## 周辺尤度の最大化

def my_evidence_optim(par):
  k_scale = np.exp(par[0])
  k_sigma = np.exp(par[1])
  K11 = my_kernel_train(k_sigma,k_scale)
  n = len(y_train)
  f = np.linspace(0,1,n)
  evidence = 0
  while True:
    prob = my_sigmoid(y_train,f)
    V1 = my_sigmoid_log_1st(y_train,f,prob)
    V2 = my_sigmoid_log_2nd(y_train,f,prob)
    V2q = np.sqrt(np.maximum(V2,0))
    B = np.identity(n) + K11 * V2q * V2q.reshape(-1,1)
    U = cholesky(B)
    b = V2 * f + V1
    a = b - solve_triangular(U,solve_triangular(U,np.dot(K11,b) * V2q,trans=1)) * V2q
    f0 = f
    f = np.dot(K11,a)
    if np.max(np.abs(f - f0)) < 1e-5:
      evidence = -np.dot(a,f) / 2 + np.sum(np.log(prob)) - np.sum(np.log(np.diag(U)))
      break
  return -evidence

start = time.time()
par = np.array([2.85,2.35])
ret = minimize(my_evidence_optim,par,method='Nelder-Mead')
print(time.time() - start)
print(ret)

C#

.matファイルの入力にはAccord.Mathを使用。
行列計算と最適化にはMathNet.Numericsを使用。
データの抽出と結合に少し手間がかかる。

class BinaryHandwrittenDigit
{
    (Vector<double>, Matrix<double>) Select(Int16[,] labels, Double[,] patterns)
    {
        int nrow = patterns.GetLength(0);
        int ncol = patterns.GetLength(1);
        int count_3 = 0;
        int count_5 = 0;
        for (int j = 0; j < ncol; j++)
        {
            if (labels[3, j] == 1) count_3++;
            else if (labels[5, j] == 1) count_5++;
        }
        Vector<double> y = new DenseVector(count_3 + count_5);
        Matrix<double> x = new DenseMatrix(nrow, count_3 + count_5);
        int i = 0;
        for (int j = 0; j < ncol; j++)
        {
            if (labels[3, j] == 1)
            {
                for (int k = 0; k < nrow; k++) x[k, i] = patterns[k, j];
                y[i++] = 1;
            }
            else if (labels[5, j] == 1)
            {
                for (int k = 0; k < nrow; k++) x[k, i] = patterns[k, j];
                y[i++] = -1;
            }
        }
        return (y, x);
    }
    Vector<double> y_train;
    Matrix<double> x_train;
    void ReadMat()
    {
        string path_to_file = "usps_resampled.mat";
        var reader = new MatReader(path_to_file);
        var train_labels = reader.Read<Int16[,]>("train_labels");
        var train_patterns = reader.Read<Double[,]>("train_patterns");
        (y_train, x_train) = Select(train_labels, train_patterns);
    }
    Matrix<double> x_train_L2;
    Matrix<double> MakeMatrixL2(Matrix<double> x)
    {
        int m = x.ColumnCount;
        var result = new DenseMatrix(m, m);
        for (int j = 0; j < m; j++)
        {
            var cj = x.Column(j);
            for (int i = 0; i < m; i++)
            {
                var s = x.Column(i) - cj;
                result[i, j] = s * s;
            }
        }
        return result;
    }
    Matrix<double> kernel_train(double k_sigma, double k_scale)
    {
        return (x_train_L2 / (-2 * k_scale * k_scale)).PointwiseExp() * (k_sigma * k_sigma);
    }
    Vector<double> sigmoid(Vector<double> y, Vector<double> f)
    {
        return y.PointwiseMultiply(f).Map((x) => Normal.CDF(0, 1, x));
    }
    Vector<double> sigmoid_log_1st(Vector<double> y, Vector<double> f, Vector<double> prob)
    {
        var df = f.Map((x) => Normal.PDF(0, 1, x));
        return y.PointwiseDivide(prob).PointwiseMultiply(df);
    }
    Vector<double> sigmoid_log_2nd(Vector<double> y, Vector<double> f, Vector<double> prob)
    {
        var df = f.Map((x) => Normal.PDF(0, 1, x));
        var a = df.PointwiseDivide(prob);
        return a.PointwiseMultiply(a + y.PointwiseMultiply(f));
    }
    Matrix<double> bXbPlusOne(Matrix<double> X, Vector<double> b)
    {
        int n = X.RowCount;
        var result = X.Clone();
        for (int j = 0; j < n; j++)
        {
            for (int i = 0; i < n; i++)
            {
                result[i, j] *= b[i] * b[j];
            }
            result[j, j] += 1;
        }
        return result;
    }
    double GetEvidence(Vector<double> par)
    {
        double k_scale = Math.Exp(par[0]);
        double k_sigma = Math.Exp(par[1]);
        var K11 = kernel_train(k_sigma, k_scale);
        int n = y_train.Count;
        Vector<double> f = new DenseVector(n);
        for (int i = 0; i < n; i++)
        {
            f[i] = 1.0 / (n - 1) * i;
        }
        double evidence = 0;
        while (true)
        {
            var prob = sigmoid(y_train, f);
            var V1 = sigmoid_log_1st(y_train, f, prob);
            var V2 = sigmoid_log_2nd(y_train, f, prob);
            var V2q = V2.PointwiseMaximum(0).PointwiseSqrt();
            var B = bXbPlusOne(K11, V2q);
            var b = V2.PointwiseMultiply(f) + V1;
            var c = K11.Multiply(b).PointwiseMultiply(V2q);
            var chol = B.Cholesky();
            var a = b - chol.Solve(c).PointwiseMultiply(V2q);
            var f0 = f;
            f = K11 * a;
            if ((f - f0).AbsoluteMaximum() < 1e-5)
            {
                evidence = -(a * f) / 2 + prob.PointwiseLog().Sum() - Math.Log(chol.Determinant) / 2;
                break;
            }
        }
        return -evidence;
    }
    public void MarginalLikelihoodMaximization()
    {
        ReadMat();
        x_train_L2 = MakeMatrixL2(x_train);
        var evidence_optim = ObjectiveFunction.Value(GetEvidence);
        Stopwatch sw = Stopwatch.StartNew();
        var ret = NelderMeadSimplex.Minimum(evidence_optim, new DenseVector(new[] { 2.85, 2.35 }));
        sw.Stop();
        Console.WriteLine("{0}sec", (double)(sw.ElapsedMilliseconds) / 1000);
        var par_m = ret.MinimizingPoint;
        Console.WriteLine("par={0},{1}", par_m[0], par_m[1]);
        Console.WriteLine("Value={0},Reason={1},Iterations={2}", ret.FunctionInfoAtMinimum.Value, ret.ReasonForExit, ret.Iterations);
    }
}
static void Main(string[] args)
{
    Control.MaxDegreeOfParallelism = 1;
    var sample = new BinaryHandwrittenDigit();
    sample.MarginalLikelihoodMaximization();
}

C++

データは、C#で出力したものを入力。省略。
最適化関数nmminは、Rのソース/src/appl/optim.cを流用して作成。省略。
Eigenでautoを使用すると型が変わってしまう。

MKLを使用する場合は、#define EIGEN_USE_BLASとして、
mkl_intel_lp64.lib,mkl_sequential.lib,mkl_core.libをリンクする。
Intel® Math Kernel Library Link Line Advisor

#include <stdio.h>
#include <direct.h>

#include <chrono>
#include <cmath>

//#define EIGEN_USE_BLAS
#include "Eigen/Dense"

using namespace std;
using namespace Eigen;

typedef double optimfn(int n, const double *x0, void *ex);

void nmmin(int n, double *Bvec, double *X, double *Fmin, optimfn fminfn,
    int *fail, double abstol, double intol, void *ex,
    double alpha, double bet, double gamm, int trace,
    int *fncount, int maxit);

double Normal_PDF(double value)
{
    const double pi = 3.1415926535897932384626433832795;
    return exp(-value * value / 2) / sqrt(2 * pi);
}
double Normal_CDF(double value)
{
    return erfc(-value / sqrt(2)) / 2;
}

class BinaryHandwrittenDigit {
private:
    int nrow;
    int ncol;
    MatrixXd x_train;
    ArrayXd y_train;
    MatrixXd x_train_L2;
public:
    void read_data();
    void make_matrix_L2() {
        x_train_L2.resize(ncol, ncol);
        for (int j = 0; j < ncol; j++) {
            for (int i = 0; i < ncol; i++) {
                double t = 0;
                for (int k = 0; k < nrow; k++) t += (x_train(k, i) - x_train(k, j)) * (x_train(k, i) - x_train(k, j));
                x_train_L2(i, j) = t;
            }
        }
    }
    MatrixXd kernel_train(double k_sigma, double k_scale) {
        return (x_train_L2 / (-2 * k_scale * k_scale)).array().exp() * (k_sigma * k_sigma);
    }
    ArrayXd sigmoid(const ArrayXd& y, const ArrayXd& f) {
        ArrayXd result(y.size());
        for (int j = 0; j < y.size(); j++) {
            result(j) = Normal_CDF(y(j) * f(j));
        }
        return result;
    }
    ArrayXd sigmoid_log_1st(const ArrayXd& y, const ArrayXd& f, const ArrayXd& prob) {
        ArrayXd result(y.size());
        for (int j = 0; j < y.size(); j++) {
            result(j) = y(j) * Normal_PDF(f(j)) / prob(j);
        }
        return result;
    }
    ArrayXd sigmoid_log_2nd(const ArrayXd& y, const ArrayXd& f, const ArrayXd& prob) {
        ArrayXd result(y.size());
        for (int j = 0; j < y.size(); j++) {
            double a = Normal_PDF(f(j)) / prob(j);
            result(j) = a * (a + y(j) * f(j));
        }
        return result;
    }
    MatrixXd bXbPlusOne(const MatrixXd& X, const ArrayXd& b)
    {
        int n = X.rows();
        MatrixXd result(X);
        for (int j = 0; j < n; j++)
        {
            for (int i = 0; i < n; i++)
            {
                result(i, j) *= b[i] * b[j];
            }
            result(j, j) += 1;
        }
        return result;
    }
    double get_evidence(const double* par) {
        double k_scale = exp(par[0]);
        double k_sigma = exp(par[1]);
        MatrixXd K11 = kernel_train(k_sigma, k_scale);
        int n = y_train.size();
        ArrayXd f = ArrayXd::LinSpaced(n, 0, 1);
        double evidence = 0;
        while (true) {
            ArrayXd prob = sigmoid(y_train, f);
            ArrayXd V1 = sigmoid_log_1st(y_train, f, prob);
            ArrayXd V2 = sigmoid_log_2nd(y_train, f, prob);
            ArrayXd V2q = V2.cwiseMax(0.0).cwiseSqrt();
            MatrixXd B = bXbPlusOne(K11, V2q);
            ArrayXd b = V2 * f + V1;
            ArrayXd c = (K11 * VectorXd(b)).array() * V2q;
            LLT<MatrixXd> chol(B);
            ArrayXd a = b - chol.solve(VectorXd(c)).array() * V2q;
            ArrayXd f0 = f;
            f = K11 * VectorXd(a);
            if ((f - f0).abs().maxCoeff() < 1e-5) {
                evidence = -(a * f).sum() / 2 + prob.log().sum() - log(chol.matrixL().determinant());
                break;
            }
        }
        return -evidence;
    }
public:
    static double get_evidence_optim(int n, const double *x, void *ex) {
        auto p = (BinaryHandwrittenDigit*)ex;
        return p->get_evidence(x);
    }
};

int main()
{
    auto sample = new BinaryHandwrittenDigit();
    sample->read_data();
    sample->make_matrix_L2();

    double ninf = -INFINITY;
    double eps = 2.220446e-16;

    ArrayXd xini(2); xini << 2.85, 2.35;
    ArrayXd xout(2);
    double fmin = 0.0;
    int fail = 0;
    double abstol = ninf;
    double intol = sqrt(eps);
    void *ex = sample;
    double alpha = 1.0;
    double bet = 0.5;
    double gamm = 2.0;
    int trace = 0;
    int fncount = 0;
    int maxit = 500;

    auto start = chrono::system_clock::now();
    nmmin(xini.size(), xini.data(), xout.data(), &fmin, BinaryHandwrittenDigit::get_evidence_optim, &fail, abstol, intol, ex, alpha, bet, gamm, trace, &fncount, maxit);
    auto end = chrono::system_clock::now();
    long elapsed = chrono::duration_cast<chrono::milliseconds>(end - start).count();
    printf("fmin=%f, fncount=%d, fail=%d\n", fmin, fncount, fail);
    for (int i = 0; i < xout.size(); i++) printf("%d, %f\n", i, xout[i]);
    printf("%d msec\n", elapsed);

    delete sample;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【WPF】部分一致絞り込みが可能なComboBoxの作り方

項目数が多いComboBoxでは部分一致絞り込みが欲しくなる

WPFに限らずですが、あらかじめ決められた項目の中からユーザーに1つ選択させるためのコントロールとして、ComboBoxというものが標準で用意されています。「プルダウン」とも呼ばれたりすることがあるやるです。
しかし、選択肢が多くなればなるほど、多数の中から目的の項目を探し出す必要が生じるため、ただのComboBoxは使い勝手が悪くなります。

そのようなときにあると便利なものが、入力した文字列と部分一致する項目のみに自動的に絞り込んでくれるComboBoxだったりします。

大まかな作成手順

ざっくり纏めると、下記の手順となります。

  1. ComboBoxを継承したコントロール(Xamlとコードビハインド)を作成する。
  2. 作成したコントロールをComboBoxと同じような感覚で使用する。

以上。

作成手順の詳細

例として、空のWPFプロジェクト(プロジェクト名はTestBenchWPFとする)に作成していきます。

1. 自作のコントロール用のフォルダを作成する。

プロジェクトを右クリック →「追加」→「新しいフォルダー」で作成します。
フォルダ名は「Controls」とします。
image.png

2. Controlsフォルダ内に、新規のユーザーコントロールを作成する。

Controlsフォルダを右クリック →「追加」→「ユーザーコントロール」で作成します。
名前は「PartialSearchComboBox」とします。
image.png

3. PartialSearchComboBox.xamlを修正する。
BEFORE
<UserControl x:Class="TestBenchWPF.Controls.PartialSearchComboBox"
             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:local="clr-namespace:TestBenchWPF.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>

    </Grid>
</UserControl>
AFTER
<ComboBox x:Class="TestBenchWPF.Controls.PartialSearchComboBox"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          IsEditable="True">
    <!-- ↑任意の文字列を入力可能にするため、IsEditableをTrueにすることは必須。 -->
</ComboBox>
4. PartialSearchComboBox.xaml.csを修正する。

以下のように実装します。

PartialSearchComboBox.xaml.cs
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace TestBenchWPF.Controls
{
    /// <summary>
    /// PartialSearchComboBox.xaml の相互作用ロジック
    /// </summary>
    public partial class PartialSearchComboBox : ComboBox
    {
        private TextBox _textBox = null;
        private Popup _popUp = null;

        public PartialSearchComboBox()
        {
            InitializeComponent();

            this.Loaded += delegate
            {
                _textBox = this.Template.FindName("PART_EditableTextBox", this) as TextBox;
                _popUp = this.Template.FindName("PART_Popup", this) as Popup;

                if (_textBox != null)
                {
                    _textBox.TextChanged += delegate
                    {
                        if (!_popUp.IsOpen && string.IsNullOrEmpty(_textBox.Text))
                        {
                            // プログラムの処理によってコンボボックス内のテキストが空に上書きされた場合、ここに入る。
                            // この処理がないと、コンボボックス内のテキストを空に上書きするときにプルダウンが開いてしまう。
                            this.Items.Filter += obj =>
                            {
                                // プルダウン部分へ適用されているフィルターを解除する。
                                return true;
                            };

                            return;
                        }

                        // 入力がある都度、即時フィルターをかける。
                        this.Items.Filter += obj =>
                        {
                            if (!(obj is ComboBoxItem))
                            {
                                return true;
                            }

                            var item = obj as ComboBoxItem;
                            if (((string)item.Content).Contains(_textBox.Text))
                            {
                                //「選択肢の文字列の中に入力された文字列が含まれる場合」にフィルターを通過させる。
                                // フィルターを通過すると、展開された選択肢の中に表示される。
                                return true;
                            }

                            return false;
                        };

                        _popUp.IsOpen = true;
                    };

                    _textBox.GotFocus += delegate
                    {
                        if (!_popUp.IsOpen)
                        {
                            // コンボボックスの入力欄にフォーカスが当たったとき、プルダウンを展開する。
                            _popUp.IsOpen = true;
                        }
                    };
                }
            };
        }
    }
}

作成したComboBoxの使い方

上記手順で作成したPartialSearchComboBoxを実際に使ってみます。
簡単な例として、MainWindow.xamlの上に都道府県のコンボボックスとして設置してみます。

BEFORE
<Window x:Class="TestBenchWPF.MainWindow"
        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"
        xmlns:local="clr-namespace:TestBenchWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
    </Grid>
</Window>
AFTER
<Window x:Class="TestBenchWPF.MainWindow"
        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"
        xmlns:local="clr-namespace:TestBenchWPF"
        xmlns:cc="clr-namespace:TestBenchWPF.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <cc:PartialSearchComboBox HorizontalAlignment="Center" VerticalAlignment="Center"
                                  Height="32" Width="120">
            <ComboBoxItem Content="北海道"/>
            <ComboBoxItem Content="青森県"/>
            <!-- 中略 -->
            <ComboBoxItem Content="鹿児島県"/>
            <ComboBoxItem Content="沖縄県"/>
        </cc:PartialSearchComboBox>
    </Grid>
</Window>

動作のサンプル

これをビルド・実行してComboBox上に任意の文字列を入力すると、部分一致する都道府県のみに絞り込まれるようになることが分かります。

例1 『川』と入力した場合
image.png

例2 『京』と入力した場合
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【WPF】部分一致インクリメンタルサーチが可能なComboBoxの作り方

項目数が多いComboBoxでは「部分一致検索」が欲しくなる

WPFに限らずですが、あらかじめ決められた項目の中からユーザーに1つ選択させるためのコントロールとして、ComboBoxというものが標準で用意されています。「プルダウン」とも呼ばれたりすることがあるやるです。
しかし、選択肢が多くなればなるほど、多数の中から目的の項目を探し出す必要が生じるため、ComboBoxは使い勝手が悪くなります。

そのようなときにあると便利なものが、入力した文字列と部分一致する項目のみに自動的に絞り込んでくれるComboBoxだったりします。
本記事では、そのようなComboBoxの手軽な作り方をご紹介します。

大まかな手順

ざっくり纏めると、下記の手順となります。

  1. ComboBoxを継承したコントロール(Xamlとコードビハインド)を作成する。
  2. 作成したコントロールをComboBoxと同じような感覚で使用する。

以上。

作成手順

例として、空のWPFプロジェクト(プロジェクト名はTestBenchWPFとする)に作成していきます。

1. 自作のコントロール用のフォルダを作成する。

プロジェクトを右クリック →「追加」→「新しいフォルダー」で作成します。
フォルダ名は「Controls」とします。
image.png

2. Controlsフォルダ内に、新規のユーザーコントロールを作成する。

Controlsフォルダを右クリック →「追加」→「ユーザーコントロール」で作成します。
名前は「PartialSearchComboBox」とします。
image.png

3. PartialSearchComboBox.xamlを修正する。
BEFORE
<UserControl x:Class="TestBenchWPF.Controls.PartialSearchComboBox"
             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:local="clr-namespace:TestBenchWPF.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>

    </Grid>
</UserControl>
AFTER
<ComboBox x:Class="TestBenchWPF.Controls.PartialSearchComboBox"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          IsEditable="True">
    <!-- ↑任意の文字列を入力可能にするため、IsEditableをTrueにすることは必須。 -->
</ComboBox>
4. PartialSearchComboBox.xaml.csを修正する。

以下のように実装します。

PartialSearchComboBox.xaml.cs
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace TestBenchWPF.Controls
{
    /// <summary>
    /// PartialSearchComboBox.xaml の相互作用ロジック
    /// </summary>
    public partial class PartialSearchComboBox : ComboBox
    {
        private TextBox _textBox = null;
        private Popup _popUp = null;

        public PartialSearchComboBox()
        {
            InitializeComponent();

            this.Loaded += delegate
            {
                _textBox = this.Template.FindName("PART_EditableTextBox", this) as TextBox;
                _popUp = this.Template.FindName("PART_Popup", this) as Popup;

                if (_textBox != null)
                {
                    _textBox.TextChanged += delegate
                    {
                        if (!_popUp.IsOpen && string.IsNullOrEmpty(_textBox.Text))
                        {
                            // プログラムの処理によってコンボボックス内のテキストが空に上書きされた場合、ここに入る。
                            // この処理がないと、コンボボックス内のテキストを空に上書きするときにプルダウンが開いてしまう。
                            this.Items.Filter += obj =>
                            {
                                // プルダウン部分へ適用されているフィルターを解除する。
                                return true;
                            };

                            return;
                        }

                        // 入力がある都度、即時フィルターをかける。
                        this.Items.Filter += obj =>
                        {
                            if (!(obj is ComboBoxItem))
                            {
                                return true;
                            }

                            var item = obj as ComboBoxItem;
                            if (((string)item.Content).Contains(_textBox.Text))
                            {
                                //「選択肢の文字列の中に入力された文字列が含まれる場合」にフィルターを通過させる。
                                // フィルターを通過すると、展開された選択肢の中に表示される。
                                return true;
                            }

                            return false;
                        };

                        _popUp.IsOpen = true;
                    };

                    _textBox.GotFocus += delegate
                    {
                        if (!_popUp.IsOpen)
                        {
                            // コンボボックスの入力欄にフォーカスが当たったとき、プルダウンを展開する。
                            _popUp.IsOpen = true;
                        }
                    };
                }
            };
        }
    }
}

使い方

上記手順で作成したPartialSearchComboBoxを実際に使ってみます。
簡単な例として、MainWindow.xamlの上に都道府県のコンボボックスとして設置してみます。

BEFORE
<Window x:Class="TestBenchWPF.MainWindow"
        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"
        xmlns:local="clr-namespace:TestBenchWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
    </Grid>
</Window>
AFTER
<Window x:Class="TestBenchWPF.MainWindow"
        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"
        xmlns:local="clr-namespace:TestBenchWPF"
        xmlns:cc="clr-namespace:TestBenchWPF.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <cc:PartialSearchComboBox HorizontalAlignment="Center" VerticalAlignment="Center"
                                  Height="32" Width="120">
            <ComboBoxItem Content="北海道"/>
            <ComboBoxItem Content="青森県"/>
            <!-- 中略 -->
            <ComboBoxItem Content="鹿児島県"/>
            <ComboBoxItem Content="沖縄県"/>
        </cc:PartialSearchComboBox>
    </Grid>
</Window>

これを実行してプルダウン上に任意の文字列を入力すると、部分一致する都道府県のみに絞り込まれるようになります。

例1 『川』と入力した場合
image.png

例2 『京』と入力した場合
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パフォーマンスを本気で気にするLINQプログラマのためのオペーレータ表

標準ライブラリのLINQオペレータにはパフォーマンス向上のための様々なトリックが仕込まれている。
実装をざっと調べたので、その成果をここにまとめておく。

  • 調査対象は.Net Core 2.。
  • オーダーが同じだからと言ってパフォーマンスが同じとは限らない。
  • 結構雑な調査なので信用しすぎないように。
  • 項目について
    • シグニチャの特殊化 :オーバーロードによって最適化の度合いが異なる場合、各オーバーロードを表す。
    • ソースの特殊化 : ソースシーケンスの具象型によって最適化の度合いが異なる場合、各具象型を表す。
    • コールスタック : オペレータ利用による、MoveNext/Currentのコールスタック深さの増加分を表す。深くなるとcallvirt呼び出しが増えるためパフォーマンスが劣化する。
    • 戻り値の特殊化型 : オペレータの戻り値が他のオペレータのパフォーマンス向上に寄与するような具象型を持つとき、その具象型を列挙する。
  • オーダーの記号について
    • $n$: ソースシーケンスの要素数。複数のソースからなる場合は添え字が省略されている。
    • $k$: ソースの要素値とオペレータの引数で決まる定数。O記法ではふつうこのような値を使わないが、LINQを考える上ではなかなかに効いてくるので便宜的に導入する。

集計メソッド

メソッド名 シグニチャの特殊化 ソースの特殊化 時間計算量 備考
Aggregate

$O(n)$
Any

$O(n)$
All

$O(n)$
Average

$O(n)$
Contains

$O(n)$
Count

$O(n)$
ElementAt

$O(n)$

IPartition $O(1)$

IList $O(1)$
ElementAtOrDefault

$O(n)$

IPartition $O(1)$

IList $O(1)$
First

$O(1)$

IPartition $O(1)$

IList $O(1)$
FirstOrDefault

$O(1)$

IPartition $O(1)$

IList $O(1)$
Last

$O(n)$

IPartition $O(1)$

IList $O(1)$
LastOrDefault

$O(n)$

IPartition $O(1)$

IList $O(1)$
Max

$O(n)$
Min

$O(n)$
SequenceEqual

$O(n)$ 両シーケンスがICollectionの場合、事前に要素数チェックが入る。
両シーケンスがIListの場合、イテレータではなくインデクサでアクセスするためcallvirt呼び出しが減る。

ICollection $O(n)$

IList $O(n)$
Single

$O(1)$ シーケンスがIListの場合、Countとインデクサにより処理される。

IList $O(1)$
Sum

$O(n)$
ToArray

$O(n)$ シーケンスがIListProviderの場合、各インターフェースに実装されたアルゴリズムで処理される。

IListProvider $O(n)$
ToList

$O(n)$

IListProvider $O(n)$
ToDictionary

$O(n)$ シーケンスが配列またはListの場合、専用のアルゴリズムで処理される。
シーケンスがICollectionの場合、Countをもとに初期キャパシティを取得する。

ICollection $O(n)$

T[] $O(n)$

List $O(n)$
ToHashSet

$O(n)$

射影メソッド

メソッド名 シグニチャの特殊化 ソースの特殊化 コールスタック 時間計算量 戻り値の特殊化型 備考
最初の1件 全件
Append

1 $O(1)$ $O(n)$ AppendPrependIterator,
Iterator,
IListProvider
AppendPrependIteratorはソースと前後のリンクリストを保持する。
AppendPrependIteratorへのAppend/Prependはリンクリストを延長するだけなのでコールスタックが深くならない。

AppendPrependIterator 0 $O(1)$ $O(n)$
Prepend

1 $O(1)$ $O(n)$ AppendPrependIterator,
Iterator,
IListProvider

AppendPrependIterator 0 $O(1)$ $O(n)$
OfType

1 $O(1)$ $O(n)$

Cast

1 $O(1)$ $O(n)$

Concat

1 $O(1)$ $\sum O(n)$ ConcatIterator,
Iterator,
IListProvider
ConcatIteratorはシーケンスのリンクを保持する。
ConcatIteratorへのConcatはリンクを延長するだけなのでコールスタックが深くならない。

ConcatIterator 0 $O(1)$ $\sum O(n)$
DefaultIfEmpty

1 $O(1)$ $O(n)$

Distinct

1 $O(1)$ $O(n)$ Iterator,
IListProvider

Except

1 $O(n_{inner})$ $\sum O(n)$
Setに依存。
GroupJoin

1 $O(n_{inner})$ $\sum O(n)$
Lookupに依存。
GroupBy

1 $O(n)$ $O(n)$

Intersect

1 $O(n_{inner})$ $\sum O(n)$
Setに依存。
Join

1 $O(n_{inner})$ $\sum O(n)$
Lookupに依存。
ToLookup

1 $O(n)$ $O(n)$ IListProvider 戻り値の実体はLookup
OrderBy

1 $O(n)$ $O(n)$ IPartition,
IOrderedEnumerable

Reverse

1 $O(n)$ $O(n)$

Select Func<T, TResult>
1 $O(1)$ $O(n)$ Iterator,
IListProvider
配列、ListIListと同じオーダーだが、一部のメソッド呼び出しにより強力な最適化が働く。
セレクタにインデックスをとるオーバーロードは最適化が弱い。
Iterator 1 $O(1)$ $O(n)$ Iterator,
IListProvider
T[] 1 $O(1)$ $O(n)$ Iterator,
IPartition
List 1 $O(1)$ $O(n)$ Iterator,
IPartition
IList 1 $O(1)$ $O(n)$ Iterator,
IPartition
IPartition 1 $O(1)$ $O(n)$ Iterator,
IPartition
Func<T, int, TResult>
1 $O(1)$ $O(n)$
SelectMany Func<TSource, IEnumerable<TResult>>
1 $O(1)$ $O(n)$ Iterator,
IListProvider
セレクタにインデックスをとるオーバーロードは最適化が弱い。
Func<TSource, int, IEnumerable<TResult>>
1 $O(1)$ $O(n)$
Skip

1 $O(k)$ $O(n)$ Iterator
IIterator 0 ~ 1 $O(k)$ $O(n)$ Iterator
IPartition 0 $O(1)$ $O(n)$ IPartition
SkipWhile

1 $O(k)$ $O(n)$

SkipLast

1 $O(1)$ $O(n)$

Take

1 $O(1)$ $O(k)$ Iterator,
IPartition


IPartition 0 $O(1)$ $O(k)$ Iterator,
IPartition

IList 1 $O(1)$ $O(k)$ Iterator,
IPartition
TakeWhile

1 $O(1)$ $O(k)$

TakeLast

1 $O(n)$ $O(n)$


IPartition 0 $O(1)$ $O(k)$ Iterator,
IPartition

IList 1 $O(1)$ $O(k)$ Iterator,
IPartition
Union

1 $O(1)$ $\sum O(n)$ UnionIterator,
Iterator,
IListProvider
UnionIteratorはシーケンスのリンクを保持し、イテレート時に重複の除外を行う。ソースがUnionIteratorで、かつ同じcomparerを使っているときはリンクを追加するだけなのでコールスタックが深くならない。

UnionIterator 0 ~ 1 $O(1)$ $\sum O(n)$ UnionIterator,
Iterator,
IListProvider
Where

1 $O(k)$ $O(n)$ Iterator,
IListProvider
配列、Listは一部のメソッド呼び出しにより強力な最適化が働く。
セレクタにインデックスをとるオーバーロードは最適化が弱い。
T[] 1 $O(k)$ $O(n)$ Iterator,
IListProvider
List 1 $O(k)$ $O(n)$ Iterator,
IListProvider
Func<T, int, bool>
1 $O(k)$ $O(n)$ Iterator,
IListProvider
Zip

1 $O(1)$ $\min(O(n))$

最適化に用いられる型

Iteratorクラス

LINQオペレータで広く利用される抽象クラス。IEnumerable, IEnumeratorを実装している。
ステートとスレッドIDを管理しており、自身を生成したスレッド内かつ唯一の利用ならGetEnumeratorが自身を返すことでオブジェクト数を増やさないようになっていたりする。
ステートフィールドの解釈は子クラス側に委ねられており、OOP的にはあまりよろしくない設計となっているが、パフォーマンスを出すためには仕方ないことなのだろうか。

IListProviderインターフェース

ToArray, ToListの専用実装を用意できるようにするインターフェース。

IPartitionインターフェース

ランダムアクセス可能なシーケンスであることを表すインターフェース。参照型向けのSpanみたいなもの。

Lookupクラス

ILookupの実装。ToLookupGroupByなどで用いられる、グループキーとシーケンスの軽量な辞書。

Setクラス

軽量なハッシュセットクラス。System.Collections.Generic.HashSetはコアライブラリで使うにはリッチすぎるということだろうか。

まとめ

  • 同じオペレータを連続適用すると効率が良い(ことがそこそこある)
  • Select/Whereはインデックス込みで評価できるオーバーロードを使うと最適化が利かなくなったりする。

最後に

正直こういうの考える必要があるほどパフォーマンスが大事なら素直にforeachで書いた方がいいと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# MicroBatchFramework バッチクラスの定義に関する検証

MicroBatchFramework とは

【GitHub】MicroBatchFramework より引用

MicroBatchFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multiple contained batch program. Easy to bind argument to the simple method definition. It built on .NET Generic Host so you can configure Configuration, Logging, DI, etc can load by the standard way.

コンソールアプリケーション向けのフレームワークです。こう言ってしまうと省略しすぎですが、コマンドラインで指定されたメソッドやパラメーターを簡単な手順でバインドさせることができます。.NET Core の機能拡張に伴い、マイクロサービスなどコンソールアプリケーションの用途が増えていくことが予想される中で、このようなインフラストラクチャの拡充には非常に注目しています。

検証内容

手始めとして、バッチクラスの定義に関する検証を行いました。

  • 別アセンブリに定義したバッチクラスのメソッドを起動できるか
  • Command 属性を付与しないメソッドが存在してもよいか

MicroBatchFramework のバージョンは 1.2.0 です。

検証に使用したクラス

定義するアセンブリと名前空間が異なる次の4つのバッチクラスを定義しました。

クラス 名前空間 説明
SampleBatch1 NetCoreApp1 起動アセンブリのルート名前空間に定義したバッチクラス
SampleBatch2 NetCoreApp1.Batches 起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラス
SampleBatch3 NetCoreApp1 起動アセンブリのルート名前空間外に定義したバッチクラス
SampleBatch4 BatchLibrary1 別アセンブリに定義したバッチクラス

メソッドの処理内容はすべて同じであるため割愛しています。コンソールにログを出力するだけです。

// 起動アセンブリのルート名前空間に定義したバッチクラス
namespace NetCoreApp1
{
    class SampleBatch1 : BatchBase
    {
        [Command("exec1")]
        public void Execute(string name1, int repeat1 = 3)
        {
            for (int i = 0; i < repeat1; i++)
            {
                this.Context.Logger.LogInformation($"{this.GetType().Name}.Execute from {name1} ({i + 1}/{repeat})");
            }
        }
    }
}

// 起動アセンブリのルート名前空間の配下に定義したバッチクラス
namespace NetCoreApp1.Batches
{
    class SampleBatch2 : BatchBase
    {
        [Command("exec2")]
        public void Execute(string name2, int repeat2 = 3) {}
    }
}

// 起動アセンブリのルート名前空間外に定義したバッチクラス
namespace Batches
{
    class SampleBatch3 : BatchBase
    {
        [Command("exec3")]
        public void Execute(string name3, int repeat3 = 3) {}
    }
}

// 別アセンブリに定義したバッチクラス
namespace BatchLibrary1
{
    public class SampleBatch4 : BatchBase
    {
        [Command("exec4")]
        public void Execute(string name4, int repeat4 = 3) {}
    }
}

検証に使用したバッチファイル

list, help コマンドと、バッチクラスに定義したメソッドを実行します。

@echo ===== list =====
@dotnet NetCoreApp1.dll list


@echo ===== help =====
@dotnet NetCoreApp1.dll help


@echo ===== exec1 =====
@dotnet NetCoreApp1.dll exec1 -name1 "test" -Repeat1 3

@echo ===== SampleBatch1.Execute =====
@dotnet NetCoreApp1.dll SampleBatch1.Execute -name1 "test" -Repeat1 3

@echo ===== exec2 =====
@dotnet NetCoreApp1.dll exec2 -name2 "test" -Repeat2 3

@echo ===== SampleBatch2.Execute =====
@dotnet NetCoreApp1.dll SampleBatch2.Execute -name2 "test" -Repeat2 3

@echo ===== exec3 =====
@dotnet NetCoreApp1.dll exec3 -name3 "test" -Repeat3 3

@echo ===== SampleBatch3.Execute =====
@dotnet NetCoreApp1.dll SampleBatch3.Execute -name3 "test" -Repeat3 3

@echo ===== exec4 =====
@dotnet NetCoreApp1.dll exec4 -name4 "test" -Repeat4 3

@echo ===== SampleBatch4.Execute =====
@dotnet NetCoreApp1.dll SampleBatch4.Execute -name4 "test" -Repeat4 3
@dotnet NetCoreApp1.dll BatchLibrary1.SampleBatch4.Execute -name4 "test" -Repeat4 3

ホストの起動方法

バッチクラスを指定せずに起動

RunBatchEngineAsync メソッドで起動します。

static async Task Main(string[] args)
{
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync(args);
}

バッチクラスの型を指定して起動

RunBatchEngineAsync<T> メソッドで起動します。

static async Task Main(string[] args)
{
    // 起動アセンブリのルート名前空間の配下に定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<SampleBatch1>(args);
}

static async Task Main(string[] args)
{
    // 起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<Batches.SampleBatch2>(args);
}

static async Task Main(string[] args)
{
    // 起動アセンブリのルート名前空間外に定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<global::Batches.SampleBatch3>(args);
}

static async Task Main(string[] args)
{
    // 別アセンブリに定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<BatchLibrary1.SampleBatch4>(args);
}

実行結果

list コマンド

  • 起動アセンブリに定義されているバッチクラスのメソッドが列挙されます。
  • 起動アセンブリ以外のアセンブリに定義されているバッチクラスを指定して起動した場合、そのアセンブリに定義されているバッチクラスも列挙対象に追加されるようです。
実行コマンド
@echo ===== list =====
@dotnet NetCoreApp1.dll list
バッチクラスを指定せずに起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
別アセンブリに定義したバッチクラスSampleBatch4を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
SampleBatch4.Execute

helpコマンド

  • RunBatchEngineAsync メソッドでホストを起動した場合
    • エラーが発生します。
    • help の後にメソッド名を指定すれば、そのメソッドのヘルプを表示することができます。メソッドに対して付与した Command 属性のコマンド名を指定した場合はエラーになります。
  • RunBatchEngineAsync<T> メソッドでホストを起動した場合
    • T に指定したバッチクラスのメソッドのヘルプが列挙されます。
@echo ===== help =====
@dotnet NetCoreApp1.dll help
バッチクラスを指定せずに起動
===== help =====
Type or method does not found on this Program. args: help
バッチクラスを指定せずに起動(helpの後にメソッド名を指定)
> dotnet NetCoreApp1.dll help SampleBatch1.Execute
exec1:
-name1: String
-repeat1: [default=3]Int32
起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動
===== help =====
exec1:
-name1: String
-repeat1: [default=3]Int32
起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動
===== help =====
exec2:
-name2: String
-repeat2: [default=3]Int32
起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動
===== help =====
exec3:
-name3: String
-repeat3: [default=3]Int32
別アセンブリに定義したバッチクラスSampleBatch4を指定して起動
===== help =====
exec4:
-name4: String
-repeat4: [default=3]Int32

メソッド実行

  • RunBatchEngineAsync メソッドでホストを起動した場合
    • 起動アセンブリに定義されているバッチクラスのメソッドを実行できます。参照アセンブリに定義されているバッチクラスは実行できません。
    • コマンドラインで指定するメソッド名には、そのメソッドのクラス名と型名({クラス名}.{メソッド名})を指定します。
  • RunBatchEngineAsync<T> メソッドでホストを起動した場合
    • T で指定したバッチクラスのメソッドを実行できます。
    • コマンドラインで指定するメソッド名には、そのメソッドに対して Command 属性で設定したコマンド名を指定します。
実行コマンド
@echo ===== exec1 =====
@dotnet NetCoreApp1.dll exec1 -name1 "test" -Repeat1 3

@echo ===== SampleBatch1.Execute =====
@dotnet NetCoreApp1.dll SampleBatch1.Execute -name1 "test" -Repeat1 3

@echo ===== exec2 =====
@dotnet NetCoreApp1.dll exec2 -name2 "test" -Repeat2 3

@echo ===== SampleBatch2.Execute =====
@dotnet NetCoreApp1.dll SampleBatch2.Execute -name2 "test" -Repeat2 3

@echo ===== exec3 =====
@dotnet NetCoreApp1.dll exec3 -name3 "test" -Repeat3 3

@echo ===== SampleBatch3.Execute =====
@dotnet NetCoreApp1.dll SampleBatch3.Execute -name3 "test" -Repeat3 3

@echo ===== exec4 =====
@dotnet NetCoreApp1.dll exec4 -name4 "test" -Repeat4 3

@echo ===== SampleBatch4.Execute =====
@dotnet NetCoreApp1.dll SampleBatch4.Execute -name4 "test" -Repeat4 3
@dotnet NetCoreApp1.dll BatchLibrary1.SampleBatch4.Execute -name4 "test" -Repeat4 3
バッチクラスを指定せずに起動
===== exec1 =====
Type or method does not found on this Program. args: exec1 -name1 test -Repeat1 3

===== SampleBatch1.Execute =====
SampleBatch1.Execute from test (1/3)
SampleBatch1.Execute from test (2/3)
SampleBatch1.Execute from test (3/3)

===== exec2 =====
Type or method does not found on this Program. args: exec2 -name2 test -Repeat2 3

===== SampleBatch2.Execute =====
SampleBatch2.Execute from test (1/3)
SampleBatch2.Execute from test (2/3)
SampleBatch2.Execute from test (3/3)

===== exec3 =====
Type or method does not found on this Program. args: exec3 -name3 test -Repeat3 3

===== SampleBatch3.Execute =====
SampleBatch3.Execute from test (1/3)
SampleBatch3.Execute from test (2/3)
SampleBatch3.Execute from test (3/3)

===== exec4 =====
Type or method does not found on this Program. args: exec4 -name4 test -Repeat4 3

===== SampleBatch4.Execute =====
Type or method does not found on this Program. args: SampleBatch4.Execute -name4 test -Repeat4 3
Type or method does not found on this Program. args: BatchLibrary1.SampleBatch4.Execute -name4 test -Repeat4 3
起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動
===== exec1 =====
SampleBatch1.Execute from test (1/3)
SampleBatch1.Execute from test (2/3)
SampleBatch1.Execute from test (3/3)

===== SampleBatch1.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== exec2 =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== SampleBatch2.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== exec3 =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== SampleBatch3.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== exec4 =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== SampleBatch4.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

exec1:
-name1: String
-repeat1: [default=3]Int32
起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動
===== exec1 =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== SampleBatch1.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== exec2 =====
SampleBatch2.Execute from test (1/3)
SampleBatch2.Execute from test (2/3)
SampleBatch2.Execute from test (3/3)

===== SampleBatch2.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== exec3 =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== SampleBatch3.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== exec4 =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== SampleBatch4.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

exec2:
-name2: String
-repeat2: [default=3]Int32
起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動
===== exec1 =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== SampleBatch1.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== exec2 =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== SampleBatch2.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== exec3 =====
SampleBatch3.Execute from test (1/3)
SampleBatch3.Execute from test (2/3)
SampleBatch3.Execute from test (3/3)

===== SampleBatch3.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== exec4 =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== SampleBatch4.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

exec3:
-name3: String
-repeat3: [default=3]Int32
別アセンブリに定義したバッチクラスSampleBatch4を指定して起動
===== exec1 =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== SampleBatch1.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== exec2 =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== SampleBatch2.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== exec3 =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== SampleBatch3.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== exec4 =====
SampleBatch4.Execute from test (1/3)
SampleBatch4.Execute from test (2/3)
SampleBatch4.Execute from test (3/3)

===== SampleBatch4.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

exec4:
-name4: String
-repeat4: [default=3]Int32

Command 属性を付与しないメソッド

Command 属性はメソッドに対してコマンド名や説明を設定するための属性ですが、この属性が付与されていないメソッドが存在した場合、若干挙動が変わります。

namespace NetCoreApp1
{
    class SampleBatch1 : BatchBase
    {
        [Command("exec1")]
        public void Execute(string name1, int repeat1 = 3) {}
        // Command属性を付与しない
        public void ExecuteNotCommand(string name, int repeat = 3) {}
    }
}

list コマンド

  • Command 属性が付与されていないメソッドも列挙対象に含まれます。

help メソッド

  • RunBatchEngineAsync メソッドでホストを起動した場合
    • Command 属性が付与されていないメソッドも、help コマンドの後にメソッド名({クラス名}.{メソッド名})を指定すれば表示されます。
  • RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドである場合
    • そのメソッドのヘルプも表示されますが、コマンド名には "argument list" と表示されます。コマンド名には空の string 配列が設定されているのではないかと予想されます。
  • RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドでない場合
    • そのメソッドのヘルプは表示されません。

メソッド実行

  • RunBatchEngineAsync メソッドでホストを起動した場合
    • Command 属性が付与されていないメソッドも実行できます。
  • RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドである場合
    • そのメソッドも、メソッド名({クラス名}.{メソッド名})を指定すれば実行できます。
    • T で指定された型以外のバッチクラスのメソッドを呼び出したときにコンソールに表示される内容が変わります。「コマンド名が見つからない」というエラーが表示されます。
Required parameter "name" not found in argument. args: exec2 -name2 test -Repeat1 3
  • RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドでない場合
    • そのメソッドは実行できません。

まとめ

一つのアプリケーションに複数のメソッドを実装する場合、一つのバッチクラスに集約するか複数のバッチクラスに分割するかによってホストの起動方法が変わります。コマンドラインの指定方法も変わります。

  • 一つのバッチクラスに集約
    • RunBatchEngineAsync<T> メソッドでホストを起動すれば、Command 属性で設定したコマンド名で指定するこ とができます。
    • 実行対象のバッチクラスは起動アセンブリから参照できる型であればよく、別アセンブリに定義されていても構いません。
  • 複数のバッチクラスに分割
    • RunBatchEngineAsync<T> メソッドでは単一のバッチクラスのメソッドしか実行できないため、RunBatchEngineAsync メソッドでホストを起動することになります。
    • Command 属性で設定したコマンド名ではなく、メソッド名({クラス名}.{メソッド名})指定することになります。
    • 実行対象のバッチクラスは起動アセンブリに定義する必要があります。

Command 属性はコマンド名や説明を設定できますし、付与することに対するデメリットはなさそうです。必ず付与するルールにしてもよさそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# x SendGridでメールを送信する 1

今日はSendGridを使ってメールを送ってみます。問い合わせフォームのメールからMA的なメール送信まで幅広く利用することを想定しています。利用用途は幅広く提供されている機能も多いので何度かに分けて整理したいと思います。 マイクロソフトからもドキュメントがまとめられているので、こちらのドキュメントも参照してください。https://docs.microsoft.com/en-us/azure/sendgrid-dotnet-how-to-send-email

環境

Visual Studio 2019, C#, ASP.NET Core 2.2
SendGrid API Version 3
もうすでにASP.NET Core プロジェクトがあるものとします。

SendGridとは?

SendGridはメール配信サービスです。名だたるサービスがSendGridを使ってメール配信をおこなっています。自社でメールサーバーを持つような時代は終わって、SendGridに面倒なメンテ作業を任せて、メールを送ることだけにフォーカスすることができます。

Send Grid Official Page
https://sendgrid.com/

Azureからもマーケットプレイスで購入することができます。Azure Marketplaceから購入すると月25,000通まで無料。

Azure Market Placeから購入

Azureのアカウントを持っている方はこちらのリンクからSendGridのサービスを購入。https://portal.azure.com/#create/Sendgrid.sendgrid

赤いマークがついてるフィールは必須なので埋めてください。

image.png

今日は一番右にあるフリープランで進めます

image.png

ライブラリーの準備

SendGridを使ってメールを送信するには便利なライブラリーがあるので下記のNugetコマンドから入手します。

SendGrid、C#用SDKGitHubはこちらから
https://github.com/sendgrid/sendgrid-csharp
.Net Core要のサンプルコードはこちらから
https://github.com/sendgrid/sendgrid-csharp/tree/master/ExampleCoreProject

パッケージマネージャーコンソールより

PM> Install-Package SendGrid

image.png

SendGrid Version 9.11.0 がインストールされています。

APIの準備

作成したSendGridメニューにあるManageをクリックします。
image.png

SendGridの管理画面がたちがりますので、右側にあるSettingからAPI Keysをクリックします。

image.png

右上にあるCreate API Keyをクリック。APIアクセスのアクセスレベルを選択して、名前を入力して作成します。

image.png

ここで生成されたAPI KeyをAppSettings.jsonに入力します。

image.png
image.png

これで一旦設定はおしまいです。次は実際にサービスを書いてみます。

サービスを書く

まず、メールを送るサービスである
サービスクラスとインターフェイスを書きます。

他のサービスクラスも格納するため、サービスフォルダーとインターフェイスフォルダーを作成し。 サービスフォルダーに EmailSernder.csというクラス、インターフェイスフォルダーの中にIEmailSender.cs というEmailSender のインターフェイスを作成します。

image.png

image.png

IEmailSender.cs
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;

namespace wppSample.Services.Interfaces
{
    public interface IEmailSender
    {
        Task SendEmailAsyc(SendGridMessage msg);
    }
}
EmailSender.cs
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;
using wppSample.Models;
using wppSample.Services.Interfaces;

namespace wppSample.Services
{
    public class EmailSender : IEmailSender
    {
        /// <summary>
        /// For Secure API Value
        /// </summary>
        private readonly IOptions<AppSettings> _config;

        /// <summary>
        /// Construct and get _config
        /// </summary>
        /// <param name="config"></param>
        public EmailSender(IOptions<AppSettings> config)
        {
            _config = config;
        }

        /// <summary>
        /// Send Email via SendGrid
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public async Task SendEmailAsyc(SendGridMessage msg)
        {
            var client = new SendGridClient(_config.Value.SendGrid.ApiKey);
            var response = await client.SendEmailAsync(msg);
        }
    }
}

これでサービスをDIすることによって、メールが送れる状態になっています。次は具体的にメールフォームを送信したときの処理を書いてみます。

問い合わせフォームからメールを送る

まずは問い合わせフォームのRazorページ

ContactUs.cshtml
@page
@model wppSample.Pages.ContactUsModel
@{
    ViewData["Title"] = "お問合せフォーム";
}

<div class="container py-4">
    <h1>お問合せ</h1>
    <p>
        下記のお問合せフォームをご利用ください。1-2営業日で返信させていただいています。
    </p>
    <div class="py-3">
        @using (Html.BeginForm(FormMethod.Post))
        {
            <div class="form-group">
                @Html.Label("お名前")
                @Html.TextBox("name", "", new { @class = "form-control border-color-2", @type = "Text", @style = "background-color:#FFFFEF", @placeholder = "お名前" })
            </div>
            <div class="form-group">
                @Html.Label("email", "メールアドレス", new { @class = "color-2" })
                @Html.TextBox("email", "", new { @class = "form-control border-color-2", @type = "Email", @style = "background-color:#FFFFEF", @placeholder = "メールアドレス" })
            </div>

            <div class="form-group">
                @Html.Label("inquiry", "問合せ内容", new { @class = "color-2" })
                @Html.TextArea("inquiry", "", new { @class = "form-control border-color-2", @style = "background-color:#FFFFEF; height:200px;", @placeholder = "問合せ内容" })
            </div>
            <div class="form-group">
                <input type="submit" class="btn btn-primary btn-rounded-0" value="送信" />
            </div>
        }
    </div>
</div>

OnPostの処理

ContactUs.cshtml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using SendGrid.Helpers.Mail;
using wppSample.Services.Interfaces;

namespace wppSample.Pages
{
    public class ContactUsModel : PageModel
    {
        private readonly IEmailSender _emailSender;

        /// <summary>
        /// Construct ContactUsModel
        /// </summary>
        /// <param name="emailSender"></param>
        public ContactUsModel(IEmailSender emailSender)
        {
            _emailSender = emailSender;
        }

        /// <summary>
        /// On Get Action
        /// </summary>
        public void OnGet()
        {

        }

        /// <summary>
        /// On Post Action
        /// </summary>
        /// <param name="name"></param>
        /// <param name="email"></param>
        /// <param name="inquiry"></param>
        public async Task<IActionResult> OnPostAsync(string name, string email, string inquiry)
        {
            // Send email to the customers to confirm the email sent

            var from = new EmailAddress()
            {
                Name = name,
                Email = email
            };

            var to = new EmailAddress()
            {
                Name = "Support",
                Email = "support email address"
            };

            var subject = "お問合せがありました";
            var plainCointent = name + "さんより、問合せ内容: " + inquiry;
            var htmlContent = name + "さんの問合せ内容<br/><br/>" + inquiry;

            var msg = MailHelper.CreateSingleEmail(from, to,subject,plainCointent,htmlContent);

            // Send email to the customer support so that they can help the customer
            await _emailSender.SendEmailAsyc(msg);

            ViewData["Name"] = name;
            ViewData["Email"] = email;
            ViewData["Inquiry"] = inquiry;

            return RedirectToPage("Index");
        }
    }
}

image.png

こんな感じの問合せページからメールが送れている事が確認できました。

次回はフォームのバリデーションや、本人へも問合せ内容を知らせるメールを送ったりなどリッチな問合せフォーム体験を書きたいと思います。今日は一旦SendGridを動かすところまでに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む