ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

最近のLambdicSql - 空なら消える

Sqlを動的に作成する場合には、「空なら消えてくれればいいのに」があります。
LambdicSqlでは以下の場合には空を渡すと要素や句が消えます。

  • Select句のメンバ
  • Join
  • Where
  • Having
  • Order By

Select句のメンバ

//Typeを表示するときのみCase式を挿入する
var type = new Sql<string>();
if (isSelectType)
{
    type = Db<DB>.Sql(db =>
        Case().
            When(db.tbl_remuneration.money < 2000).Then("Cheap").
            When(db.tbl_remuneration.money < 3000).Then("Middle").
            Else("High").
        End());
}

var sql = Db<DB>.Sql(db =>
    Select(new SelectData
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
                    
        //タイプ
        Type = type
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id)
    );

isSelectTypeが有効のときは

SELECT
        tbl_staff.name AS Name,
        tbl_remuneration.payment_date AS PaymentDate,
        tbl_remuneration.money AS Money,
        CASE
                WHEN (tbl_remuneration.money) < (@p_0)
                THEN @p_1
                WHEN (tbl_remuneration.money) < (@p_2)
                THEN @p_3
                ELSE @p_4
        END AS Type
FROM tbl_remuneration
        JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)

無効のとき、つまりtypeが空の時は消えます。

SELECT
        tbl_staff.name AS Name,
        tbl_remuneration.payment_date AS PaymentDate,
        tbl_remuneration.money AS Money
FROM tbl_remuneration
        JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)

Where、Having

これらの句は条件を動的に組み立てるユーティリティもサポートしています。
Conditionクラスは第一引数がnullなら消えます。
両方消えると句自体がなくなります。

var minCondition = false;
var maxCondition = false;

var exp = Db<DB>.Sql(db =>
    new Condition(minCondition, 3000 < db.tbl_remuneration.money) &&
    new Condition(maxCondition, db.tbl_remuneration.money < 4000));

var query = Db<DB>.Sql(db =>
    Select(Asterisk()).
    From(db.tbl_remuneration).
    Where(exp)
);
SELECT *
FROM tbl_remuneration

Join、Order By

あまりないかもしれませんが、JoinとOrder Byも消えます。

var tbl_staff = new Sql<Staff>();
var asc = new Sql<OrderByElement>();
var desc = new Sql<OrderByElement>();
var sql = Db<DB>.Sql(db =>
    Select(new SelectedData2
    {
        Id = db.tbl_remuneration.staff_id
    }).
    From(db.tbl_remuneration).
    Join(tbl_staff, db.tbl_remuneration.staff_id == tbl_staff.Body.id).
    OrderBy(asc, desc));
SELECT
	tbl_remuneration.staff_id AS Id
FROM tbl_remuneration

こんな感じで楽にSQL構築ができます。

最近のLambdicSql - 自由度Up

github.com

組み合わせの自由度がアップしました。
Sqlビルダーという位置づけになった今、Sql構築の自由度は重要です。

+演算子のサポート

ちょっと前から生成したsql同士のサポートはありました。

var select = Db<DB>.Sql(db =>
    Select(new SelectData1
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
    }));

var from = Db<DB>.Sql(db =>
    From(db.tbl_remuneration).
    Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id));

var where = Db<DB>.Sql(db =>
    Where(3000 < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000));

var orderby = Db<DB>.Sql(db =>
    OrderBy(Asc(db.tbl_staff.name)));

//結合
var sql = select + from + where + orderby;

最近ではこれをLambda中で使うことができるようになりました。

var from = Db<DB>.Sql(db =>
        From(db.tbl_remuneration).
    Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id));

//ラムダの中で結合
var sql = Db<DB>.Sql(db =>
    Select(new SelectData
    {
        Type = Case() +
                    When(db.tbl_remuneration.money < 2000).Then("Cheap") +
                    When(db.tbl_remuneration.money < 3000).Then("Middle") +
                    Else("High") +
                End()
    }) +
    from +
    Where(1000 < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000) +
    OrderBy(Asc(db.tbl_remuneration.money), Desc(db.tbl_staff.name))
    );

これにより、より自由にSQLを組み立てることができるようになります。
例えば、Case式のWhen、Thenを条件によって追加とかね。

//checkMiddleが有効の時のみWhen、Thenの句を追加
//空のSqlは文字列に変換するときに消える
var whenThen = checkMiddle ? 
                  Db<DB>.Sql(db => When(db.tbl_remuneration.money < 3000).Then("Middle")) :
                  new Sql<string>();

var sql = Db<DB>.Sql(db =>
    Select(new SelectData
    {
        Type = Case<string>() +
                    When(db.tbl_remuneration.money < 2000).Then("Cheap") +
                    whenThen +
                    Else("High") +
                End()
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id).
    Where(1000 < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000).
    OrderBy(Asc(db.tbl_remuneration.money), Desc(db.tbl_staff.name))
    );

是非お試しください!

最近のLambdicSql - PCL(Xamarin)でも使えます

github.com

この世の全てのSQLC#で表現してやる!
ってことでマルチプラットフォーム対応もしました。
.NETCoreやPCLでも使えます。

SQLiteと連携

Xamarinって言ったらSQLiteです。
LambdicSqlはSQLiteとの連携機能も入れています。
まずは、LambdicSqlとsqlite-net-pclをインストールします。
f:id:ishikawa-tatsuya:20170127094450p:plain
そして、使うところで以下をusingします。
フィーチャーしてますw

using LambdicSql.feat.SQLiteNetPcl;

これで準備完了。
以下コードです。
テーブル作って、Insertして、Selectしてます。
まあ、テーブルの生成とかInsertはSQLite自体に便利なのがあるから使うことはあんまりないと思いますが、Selectは細かくやりたいときは便利に使えると思います。サブクエリとかね。もちろん入れ子のサブクエリとかもっと複雑なのも書けますよ。

using System;
using SQLite;
using LambdicSql;
using LambdicSql.feat.SQLiteNetPcl;
using static LambdicSql.Symbol;
using System.Collections.Generic;

namespace LambdicSqlTest
{
    public class Staff
    {
        public int id { get; set; }
        public string name { get; set; }
    }

    public class Remuneration
    {
        public int id { get; set; }
        public int staff_id { get; set; }
        public DateTime payment_date { get; set; }
        public decimal money { get; set; }
    }

    public class DB
    {
        public Staff tbl_staff { get; set; }
        public Remuneration tbl_remuneration { get; set; }
    }

    public class SelectedData
    {
        public string Name { get; set; }
        public DateTime PaymentDate { get; set; }
        public decimal Money { get; set; }
        public decimal Total { get; set; }
    }

    public static class SqlSample
    {
        public static List<SelectedData> Test(SQLiteConnection con)
        {
            DeleteTablesTest(con);
            CreateTablesTest(con);
            InsertTest(con);
            return SelectTest(con);
        }

        static void DeleteTablesTest(SQLiteConnection con)
        {
            var deleteRemuneration = Db<DB>.Sql(db => DropTable(db.tbl_remuneration));
            con.Execute(deleteRemuneration);

            var deleteStaff = Db<DB>.Sql(db => DropTable(db.tbl_staff));
            con.Execute(deleteStaff);
        }

        static void CreateTablesTest(SQLiteConnection con)
        {
            var createStaff = Db<DB>.Sql(db => CreateTable(
                db.tbl_staff,
                new Column(db.tbl_staff.id, DataType.Int(), NotNull(), PrimaryKey()),
                new Column(db.tbl_staff.name, DataType.VarChar(50), NotNull())
                ));
            con.Execute(createStaff);

            var createRemuneration = Db<DB>.Sql(db => CreateTable(
                db.tbl_remuneration,
                new Column(db.tbl_remuneration.id, DataType.Int(), NotNull(), PrimaryKey()),
                new Column(db.tbl_remuneration.staff_id, DataType.Int(), NotNull()),
                new Column(db.tbl_remuneration.payment_date, DataType.VarChar(50), NotNull()),
                new Column(db.tbl_remuneration.money, DataType.Decimal(), NotNull())
                ));
            con.Execute(createRemuneration);
        }

        static void InsertTest(SQLiteConnection con)
        {
            var insertStaff = Db<DB>.Sql(db =>
                InsertInto(db.tbl_staff, db.tbl_staff.id, db.tbl_staff.name).
                Values(1, "taro-yamada"));
            con.Execute(insertStaff);

            var insertRemuneration = Db<DB>.Sql(db =>
                InsertInto(db.tbl_remuneration,
                    db.tbl_remuneration.id,
                    db.tbl_remuneration.staff_id,
                    db.tbl_remuneration.payment_date,
                    db.tbl_remuneration.money).
                Values(1, 1, DateTime.Today, 300000));
            con.Execute(insertRemuneration);
        }

        static List<SelectedData> SelectTest(SQLiteConnection con)
        {
            var min = 3000;

            var sql = Db<DB>.Sql(db =>
                Select(new SelectedData
                {
                    Name = db.tbl_staff.name,
                    PaymentDate = db.tbl_remuneration.payment_date,
                    Money = db.tbl_remuneration.money,
                    //サブクエリとかね
                    Total = Select(Sum(db.tbl_remuneration.money)).From(db.tbl_remuneration)
                }).
                From(db.tbl_remuneration).
                    Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
                Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 1000000)
                );

            return con.Query(sql);
        }
    }
}

呼び出し元です。

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    // Perform any additional setup after loading the view, typically from a nib.
    Button.AccessibilityIdentifier = "myButton";
    Button.TouchUpInside += delegate
    {
        string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "test.db3");
        var con = new SQLiteConnection(dbPath);
        var datas = SqlSample.Test(con);
        var title = string.Format("data count = {0}", datas.Count);
        Button.SetTitle(title, UIControlState.Normal);
    };
}

しかし、VS for Mac でビルドが通らない・・・

で、動かそうと思ってMacの方に持っていくと・・・
ぐはっ、コンパイル通らんやんけ!
なんも曖昧ちゃうわ!
f:id:ishikawa-tatsuya:20170127102333p:plain
なんかわからんけど、拡張メソッドが解決できないみたいです。まあ、まだプレビューなので大目に見よう・・・。
LambdicSqlは分解して書けるので以下のように書き換えました。

static List<SelectedData> SelectTest(SQLiteConnection con)
{
    var min = 3000;

    //一つづつ作って
    var select = Db<DB>.Sql(db =>
        Select(new SelectedData
        {
            Name = db.tbl_staff.name,
            PaymentDate = db.tbl_remuneration.payment_date,
            Money = db.tbl_remuneration.money,
        }));
    var from = Db<DB>.Sql(db => From(db.tbl_remuneration));
    var join = Db<DB>.Sql(db => Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id));
    var where = Db<DB>.Sql(db => Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 1000000));

    //+で結合
    return con.Query(select + from + join + where);
}

でもこれは、メソッドチェーンでなくて、+演算に変えろっていうお告げなのか?

で実行結果です。
f:id:ishikawa-tatsuya:20170127104401p:plain:w250

最近のLambdicSql - この世の全てのSQLをC#で表現する

github.com
まだβ版です。とはいえ、今度こそ着実にリリースに近づいています。
この辺で最近入れた機能をご紹介させていただきます。
それは句や関数を簡単に追加できる機能です。

この世の全てのSQLC#で表現するポテンシャルを持たせました。

若干言い過ぎ感はありますがw
LambdicSqlは多くの句や関数がデフォルトで組み込まれています。今のところの判断基準は私が読んだSQLの入門書に書かれていたものです。(えっ?)でも、もちろんこれでは足りず、かといって全網羅するのは大変だしなーってことで、ユーザー側で簡単に追加できるようにしました。
LambdicSql自体も、ここで解説している仕組みを使って関数や句を定義しているので、それがサンプルになると思います。よろしければ参照してください。

一般的な関数の追加

FuncStyleConverterAttributeをつけるだけで使えるようになります。
以下のルールで変換できます。

  • 関数名はC#のものが大文字にって使われる(Nameプロパティで別途指定も可能)
  • 引数に括弧がつく
  • 間がカンマで区切られる
//使いたい関数を定義
static class MyFuncs
{
    [FuncStyleConverter]
    internal static Cos(double angle) { throw new NotSupportedException(); }
}

それで定義の中身なのですが、実行されることはないので空っぽでOKです。(Exceptionの解析に使われるだけです)
using static MyFuncs;
を書いておくとよりSQLっぽさがでます。

//使える!
void Test()
{
    var sql = Db<DB>.Sql(db =>
        Select(new SelectedData
        {
            Val = Cos(db.tbl1.angle),
        }).
        From(db.tbl1)                
    );

    Console.WriteLine(sql.Build(_connection.GetType()).Text);
}

こんな感じで変換されます。

SELECT
        COS(tbl1.angle) AS Val
FROM tbl1

一般的な句の追加

ClauseStyleConverterAttributeを使います

  • 句名はC#のものが大文字にって使われる(Nameプロパティで別途指定も可能)
  • 句と第一引数の間はスペースで区切られる
  • 間がカンマで区切られる

句は結構実装したので良いのが思いつきませんでした。
それでLambdicSqlで定義してますがサンプルはLIMIT句を使います。

//使いたい句を定義 
static class MyFuncs
{
    [ClauseStyleConverter]
    public static ClauseChain<Non> Limit(object offset, object count) { throw new InvalitContextException(nameof(Limit)); }

    [ClauseStyleConverter]
    public static ClauseChain<TSelected> Limit<TSelected>(this ClauseChain<TSelected> before, object offset, object count) { throw new InvalitContextException(nameof(Limit)); }
}

句はどうしてもメソッドチェーンでつなげたいですよね。なのでClauseChainというクラスの拡張メソッドとして定義してもらうとOKです。それでバラバラに書いて後でくっつけたい場合もあるので拡張ではないバージョンも定義しておくと便利です。

void Test()
{
    var sql = Db<DB>.Sql(db =>
            Select(Asterisk(db.tbl_remuneration)).
            From(db.tbl_remuneration).
            OrderBy(Asc(db.tbl_remuneration.id)).
            Limit(1, 3)
            );

    Console.WriteLine(sql.Build(_connection.GetType()).Text);
}
SELECT *
FROM tbl_remuneration
ORDER BY
	tbl_remuneration.id ASC
LIMIT @p_0, @p_1

フォーマットを自分で指定するもの

とは言え、別のフォーマットのものもありますよ。ということでフォーマットを指定したい場合はMethodFormatConverterAttributeを使います。ついでにenumを使いたい場合の説明も。
TABLESAMPLEでやってみます。

static class MyFuncs
{
    [MethodFormatConverter(Format = "TABLESAMPLE [0](|[1])")]
    public static ClauseChain<Non> TableSample(SamplingMethod method, double percentage) { throw new InvalitContextException(nameof(Limit)); }

    [MethodFormatConverter(Format = "TABLESAMPLE [1](|[2])")]
    public static ClauseChain<TSelected> TableSample<TSelected>(this ClauseChain<TSelected> before, SamplingMethod method, double percentage) { throw new InvalitContextException(nameof(Limit)); }
}

[EnumToStringConverter]
public enum SamplingMethod
{
    //大文字になる
    System,

    //別途文字列指定もできる
    [FieldSqlName("BERNOULLI")]
    Bernoulli
}
void Test()
{
    var sql = Db<DB>.Sql(db =>
            Select(Asterisk(db.tbl_remuneration)).
            From(db.tbl_remuneration).
            OrderBy(Asc(db.tbl_remuneration.id)).
            Limit(1, 3)
            );

    Console.WriteLine(sql.Build(_connection.GetType()).Text);
}
SELECT *
FROM tbl_remuneration
TABLESAMPLE SYSTEM(@p_0)

Formatで指定した感じに文字列化されます。登場する記号類です。

記号 内容
| 改行が入る場合でも、その位置までは一行目に残す
[i] i番目の引数を入れる
[<, >i] i番目の引数を展開する。その引数は配列である必要がある。<>の中はセパレータを指定する。
[$i] パラメータを使わず直値でSQLに文字列として入れる
[#i] カラムが入った場合テーブル名なしでカラム名のみにする
[!i] 特殊文字列。指定された文字列を直接SQLに入れる。db名や制約名称に利用。

さらに特殊な場合

MethodConverterAttributeを継承します。実は他にもnew、フィールド、プロパティ、オブジェクトを変換することもできます。

MethodConverterAttribute メソッド
NewConverterAttribute コンストラクタ
ObjectConverterAttribute オブジェクトの変換。型に使う
MemberConverterAttribute フィールド、プロパティー

Expressionがわたってくるのでそれを解析して、コードにして返します。
LambdicSqlでもいくつかはこれを使っているのでサンプルとしてはこちらを参照お願いします。

public abstract ICode Convert(MethodCallExpression expression, ExpressionConverter converter);

仲間を募集中

ユーザー定義で増やせるとは言え、デフォルトで入ってた方が良いですよねー。なのでもっとLambdicSqlで定義されている分を増やしたい。今は一つのクラスに定義してるけど、DB別とかで定義したら、そのDBで使えるものだけインテリセンスにでて便利度UPですよね。でも一人では工数が足りない・・・。だれか一緒に、すべてのSQLをC#で表現しませんか?仲間募集中です!

Expression中の値を取り込む

C# Advent Calendar 2016の記事になります。26日ですが、今朝見たら空きがあったので書かせてもらうことにしました。
私は趣味でOSSのライブラリを作ってます。テスト自動化用のライブラリのFriendlyとか、最近はLambdicSqlというC#のラムダでSQLを作成するライブラリを作っています。
こんな感じでC#ラムダ式を書くと、そのまま文字列とパラメータになります。
Dapperと組み合わせるとそのまま実行可能です。
それからEntityFrameworkと組み合わせて使うこともできるようにしています。(β版リリース時の記事もご紹介。)

var min = 3000;

//C#からSQL作成
var query = Db<DB>.Sql(db =>

    //ここに書いたラムダが(割と)そのままSQLになる!
    Select(new SelectData()
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
    Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000)
    
);

//文字列とパラメータ作成
var info = query.Build(_connection.GetType());
Debug.Print(info.Text);

//Dapperと連携可能。Select句の型情報を持っているので、そのまま実行できるよ。
var datas = _connection.Query(query).ToList();

こんなSQLになります。

SELECT
    tbl_staff.name AS Name,
    tbl_remuneration.payment_date AS PaymentDate,
    tbl_remuneration.money AS Money
FROM tbl_remuneration
    JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)
WHERE ((@min) < (tbl_remuneration.money)) AND ((tbl_remuneration.money) < (@p_1))

例のようなシンプルなSQLだけではなくサブクエリとか内部結合とか、かなり複雑なSQLも記述可能です。
絶賛実装中なのです。当初の予定より遅れてますが、年明けくらいには1.0にする予定です。βにしたもののやってたら、やりたいこと増えていくんですよね・・・。

Expression中の値を取り込む

で、今回の本題です。Expression中の変数の取り込みに関してです。最小な感じにして以下で考えます。

var min = 100;
var query = Sql<DB>.Create(db => min < 4000);

端折りますが受けているところはこんな感じでexpression.Bodyから解析を開始します。

public static SqlExpression<TResult> Sql<TResult>(Expression<Func<TDB, TResult>> expression)
{
    //解析スタート
    expression.Body;
}

これを解析していくとこんな感じになっています。
BinaryExpression
  Left
    FieldExpression
      (min)
  Right
    ConstExpression
      Value (4000)

定数の取り込み

上の例で4000は定数です。これはコンパイル時に決まります。こういうのは解析していくとConstantExpressionという式で表されます。これは簡単でValueに値が入っているのでそれを取得できます。

object Convert(ConstantExpression constant)
	=>constant.Value;

変数の取り込み

これがポイントです。リフレクションを使うか式木をコンパイルしないと値が取れないんですよね。値が取れるとこまでExpressionを解析します。今回の場合だとすぐにConstExpressionになってValueにはminを保持しているobjectが取得できます。(本来は色々場合分けがありますが今回は端折ります)

object GetMemberObject(MemberExpression exp)
{
    var constant = member.Expression as ConstantExpression;
    var obj = constant.Value;
}

objの型は以下のものになっています。
{Test.Samples.<>c__DisplayClass73_0}
そんな型作った覚えないよって感じなのですが、コンパイル時に作られる型です。変数引き渡し用ですね。

で、objが取れるので
引数を一つとって、そのオブジェクトのメンバのminを返す式木を作ります。

var param = Expression.Parameter(obj.GetType(), "param");
var exp = Expression.PropertyOrField(param, member.Member.Name);

ここで問題なのは、objがobject型なので誰かがそれに静的な型を付与してやらないと、上手く呼び出せないことですね。キャストの式を入れても良いのですが、今回は Type Erasure というパターンを使うことにします。
静的な型が必要ないインターフェイスを用意し、それを実装するクラスが静的な型を持っているというものですね。それによって使う側は静的な型が必要なく使えます。

interface IGetter
{
    void Init(Expression exp, ParameterExpression[] param);
    object GetMemberObject(object[] arguments);
}
class GetterCore<T0> : IGetter
{
    public delegate object Func(T0 t0);
    Func _func;
    public void Init(Expression exp, ParameterExpression[] param) => _func = Expression.Lambda<Func>(exp, param).Compile();
    public object GetMemberObject(object[] arguments) => _func((T0)arguments[0]);
}
var getter = Activator.CreateInstance(typeof(GetterCore<>).MakeGenericType(type), true) as IGetter;
getter.Init(Expression.Convert(param, typeof(object)), new[] { param });
var value =  getter.GetMemberObject(new object[] { obj });

コンパイルすると遅い

このコンパイルはハイコストです。毎回やったら遅いですね。(最初は毎回やってたんですけどね・・・)なんでキャッシュします。上のではIGetterをキャッシュしておけばよいです。LambdicSqlではこの他にメソッド呼び出しとかオブジェクトの生成とかコンパイルが必要になるケースはすべてキャッシュするようにしました。それによってかなり高速にLambda→SQL変換ができるようになりました。(アドバイスをくれた皆様ありがとうございました!)

実はExpression化するだけでもコストがかかる

例えば、こんなコードでもタダではありません。私もやるまで「これはコンパイル時に決定されるよねー」とか勘違いしていたのですが、当然そんなことはなくExpressionの中に変数取り込んだりでコストは発生しているのです。しかも中の式が複雑になればなるほど、その時間は長くなるのです。

void Test()
{
    var min = 100;
    Empty(() => min < 4000);
}
static void Empty<T>(Expression<Func<T>> expression){}

つまり、がんばってパフォーマンスチューニングはしたものの限界はあります。多くのケースでは無視しても良いくらいなのですが、シビアなケースも考えLambadicSqlではユーザー側でキャッシュしやすい設計にしています。

Expression解析に興味があれば

github.com
結構Expression解析実装したのでサンプルになる部分もあるかも。興味があれば、見てみてください。
そんで、もうすぐ1.0リリースします。今もベータですが公開しているので興味があればNugetからご利用お願いします!
www.nuget.org

一日おくれですが、メリークリスマス!

VisualStudio for mac で Xamarinデビュー

初Xamarinやってみました!
しかも僕はAndroidiPhoneのプログラムも初です!

でも実はモバイルプログラム歴は3年くらいあるんですよ。
ガラケーですけどねw
ソフトバンク?なんですかそれ?僕がやってた頃はJ-PhoneとかVodafoneでしたよ。
あの頃は徹夜も散々やったなー(遠い目
まあ、昔話はこれくらいにして本編。

今回の目標

Androidの実機で動かすとこまでを目標にします。iOSもやりたいんですけど、僕まだiPhone持ってないんですよねー。

ソリューション新規作成

f:id:ishikawa-tatsuya:20161127214439p:plain:w300
まずは基本ということでNativeの方にします。 XamarinFormsはまた今度。Single View App を選択します。
f:id:ishikawa-tatsuya:20161127214544p:plain
プロジェクト名称はXamarinTestにしました。SharedCodeはPortable Class Libraryにしました。こちらはDLLとして処理を共有するものです。SharedLibraryはもっと原始的にソースコードを共有するプロジェクトです。
f:id:ishikawa-tatsuya:20161127214632p:plain
特に問題なくソリューションが作成されました。
f:id:ishikawa-tatsuya:20161127215259p:plain
プロジェクトは3つ作られています。
役割は以下のようです。直感的な構成ですね。

XamarinTest 共有コード
XamarinTest.Droid アンドロイド専用コード
XamarinTest.iOS iOS専用コード

F5

コードは全く触ってません。とりあえず、F5で実行してみます。
f:id:ishikawa-tatsuya:20161127220751p:plain:w300
おー、iOSエミュレータで起動しました。

Android

今度はAndroidでもやってみます。XamarinTest.Droidを右クリックしてスタートアッププロジェクトにします。
f:id:ishikawa-tatsuya:20161127221803p:plain:w300
でF5を押すと
f:id:ishikawa-tatsuya:20161127222001p:plain:w300

AndroidSDK更新

今日の記事ではコードは触ってませんが、生成されたコード見てると、Main.axmlを表示するときにエラーがでました。
f:id:ishikawa-tatsuya:20161127222359p:plain
アップデートしろって、こないだインストールしたばっかりやん・・・。やり方よくわからず手間取りましたが、素直にこの画面で「Open Android SDK」を選択すると SDK Manager が開いて更新できるようです。以下更新中です。
f:id:ishikawa-tatsuya:20161127222658p:plain

実機デバッグ

実機にインストールする一番簡単な方法がこれのようですね。どうやら実機でデバッグするとそのついでにデプロイされるらしいのでやってみます。

まずはAndroidデバッグモードにする

これは機種によるらしいですが、手元にあった HTC HTL 23 だと、以下の手順で画面遷移して、ビルド番号を7回タップで開発者モードにできました。
f:id:ishikawa-tatsuya:20161128072427p:plain
その後開発者向けオプションからUSBデバッグを有効にしました。ついでにデバッグ中にSleepになると不便なのでスリープを無効にしました。
f:id:ishikawa-tatsuya:20161128072714p:plain

接続

macの場合は特にドライバとか必要なく繋ぐだけでできるようになるとの噂です。では繋いでみましょう・・・。あかん!僕のmacにはUSBついてないやん!仕方ないので近所のエディオンに変換ケーブル買いに。
で、気を取り直して繋いでみました。最初どうするのかわからなかったですが、どうやら画面上部のバーのところで、実機が選択できるようになります。ここで繋いだ実機を選択します。
f:id:ishikawa-tatsuya:20161127223319p:plain
そして実行すると・・・
f:id:ishikawa-tatsuya:20161128072838p:plain:w300
で、デバッグを終了させてもアプリは残りました。一応目標達成かな。apkファイルの作り方とかはまた調べてみよっと。
f:id:ishikawa-tatsuya:20161128073138p:plain:w300

MFCのプロジェクト設定を変えてみる

なぜか今日はMFCですw
とある人から「MFC共Cのプロジェクトを<標準Windowsライブラリを使用する>に変更してもビルド通るんだけど」って話を聞きました。んー、まあもともと共有DLLをリンクしてるからありえなくもないよなーっと思って実験してみました。

MFCプロジェクトを新規作成
②プロジェクトの設定を<標準Windowsライブラリを使用する>に変更
f:id:ishikawa-tatsuya:20161123001432p:plain
そしてビルド!
f:id:ishikawa-tatsuya:20161123001459p:plain
えー、怒られたじゃん・・・。まあ、普通はそうだよねー。
とは言え、ビルドできたって言ってるんだから、やってみようじゃないですか。

_AFXDLLを定義

エラーで_AFXDLLが定義されてないよって言われてるから、無理やり定義します。
f:id:ishikawa-tatsuya:20161123001909p:plain
で、ビルドすると・・・
f:id:ishikawa-tatsuya:20161123002032p:plain
あー、そういうと思ったよ。でもめげないよー。

_tWinMainを作成

たしかappmodul.cppにそんなの定義されてたよねー。その関数だけコピってきます。
で、これをMainFrm.cppの最後に貼る。(場所はどこでもいいです。)

extern int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	_In_ LPTSTR lpCmdLine, int nCmdShow);

extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	_In_ LPTSTR lpCmdLine, int nCmdShow)
#pragma warning(suppress: 4985)
{
	// call shared/exported WinMain
	return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}

さー、ビルド!おー、通ったよ。で実行すると・・・
f:id:ishikawa-tatsuya:20161123002639p:plain
動きましたー

結論

  • 普通にやるとビルド通らない
  • 無理やりやるとビルド通って実行もできる

追伸

MFCたちもWindowsの仲間に入れてやってください・・・
f:id:ishikawa-tatsuya:20161123005416p:plain