20200802のC#に関する記事は9件です。

mp3のタグ(ID3v1, ID3v2)を読み込む(ライブラリ不使用)

前に使っていたmp3タグエディタがWindows10でなぜか正常に動作しなくなったので、自力で編集ツールを作ろうと思って調べてみた。
ID3v1は割と簡単に編集できそうだが、ID3v2は結構めんどくさそう・・・。ID3v2.2, v2.3, v2.4の微妙な仕様差異もめんどくさい・・・。

今回は、まずは単一ファイルの読み込みだけに対応してみた。

参考サイト ID3v1

参考サイト ID3v2

キャプチャ

image.png

ソースコード

pargeじゃなくてparseやんけ・・・orz (∩´∀`)∩

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;


public class ID3v1
{
    const int CodePage_Shift_JIS = 932;

    string[] Genres = {
        "Blues","ClassicRock","Country","Dance",
        "Disco","Funk","Grunge","Hip-Hop",
        "Jazz","Metal","NewAge","Oldies",
        "Other","Pop","R&B","Rap",
        "Reggae","Rock","Techno","Industrial",
        "Alternative","Ska","DeathMetal","Pranks",
        "Soundtrack","Euro-Techno","Ambient","Trip-Hop",
        "Vocal","Jazz+Funk","Fusion","Trance",
        "Classical","Instrumental","Acid","House",
        "Game","SoundClip","Gospel","Noise",
        "Alt.Rock","Bass","Soul","Punk",
        "Space","Meditative","InstrumentalPop","InstrumentalRock",
        "Ethnic","Gothic","Darkwave","Techno-Industrial",
        "Electronic","Pop-Folk","Eurodance","Dream",
        "SouthernRock","Comedy","Cult","Gangsta",
        "Top40","ChristianRap","Pop/Funk","Jungle",
        "NativeAmerican","Cabaret","NewWave","Psychadelic",
        "Rave","Showtunes","Trailer","Lo-Fi",
        "Tribal","AcidPunk","AcidJazz","Polka",
        "Retro","Musical","Rock&Roll","HardRock",
        "Folk","Folk/Rock","NationalFolk","Swing",
        "Fusion","Bebob","Latin","Revival",
        "Celtic","Bluegrass","Avantgarde","GothicRock",
        "ProgressiveRock","PsychedelicRock","SymphonicRock","SlowRock",
        "BigBand","Chorus","EasyListening","Acoustic",
        "Humour","Speech","Chanson","Opera",
        "ChamberMusic","Sonata","Symphony","BootyBass",
        "Primus","PornGroove","Satire","SlowJam",
        "Club","Tango","Samba","Folklore",
        "Ballad","Power Ballad","Rhytmic Soul","Freestyle",
        "Duet","Punk Rock","Drum Solo","Acapella",
        "Euro-House","Dance Hall","Goa","Drum & Bass",
        "Club-House","Hardcore","Terror","Indie",
        "BritPop","Negerpunk","Polsk Punk","Beat",
        "Christian Gangsta Rap","Heavy Metal","Black Metal","Crossover",
        "Contemporary Christian","Christian Rock","Merengue","Salsa",
        "Trash Metal","Anime","JPop","SynthPop",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "(reserved)","(reserved)","(reserved)","(reserved)",
        "Sacred","Northern Europe","Irish & Scottish","Scotland",
        "Ethnic Europe","Enka","Children's Song","(reserved)",
        "Heavy Rock(J)","Doom Rock(J)","J-POP(J)","Seiyu(J)",
        "Tecno Ambient(J)","Moemoe(J)","Tokusatsu(J)","Anime(J)", // 255 ("Anime(J)") は本来「不使用」がアサインされている(?)
    };

    // null終端文字列の扱い
    // https://www.ipentec.com/document/csharp-null-terminate-string-trimming
    byte[] TitleInBytes;
    byte[] ArtistInBytes;
    byte[] AlbumInBytes;
    byte[] YearInByte;
    byte[] CommnetInByte;
    byte GenreNumber; // ジャンル
    public int Year {
        get { 
            try {
                return Convert.ToInt32(Encoding.ASCII.GetString(YearInByte));
            }
            catch(Exception){return 0;}
        }
    }

    string TryToGetString(byte[] t)
    {
        try {
            return Encoding.GetEncoding(CodePage_Shift_JIS).GetString(t).TrimEnd(new char[]{'\0',' '});
        }
        catch(Exception){return "";}
    }

    public string Title   { get { return TryToGetString(TitleInBytes); } }
    public string Artist  { get { return TryToGetString(ArtistInBytes); } }
    public string Album   { get { return TryToGetString(AlbumInBytes); } }
    public string Comment { get { return TryToGetString(CommnetInByte); } }
    public string Genre {
        get {return Genres[GenreNumber];}
    }
    public int Track{get;private set;}


    public static ID3v1 PargeFromFoot(byte[] buffer)
    {
        return Parge(buffer, buffer.Length-128);
    }

    public static ID3v1 Parge(byte[] buffer, int offset)
    {
        ID3v1 ret = new ID3v1();
        if ( offset < 0 ) {
            return null;
        }
        if (buffer.Length < offset + 128 ) {
            return null;
        }

        // ヘッダチェック ("TAG")
        if ( buffer[offset] != 0x54 || buffer[offset+1] != 0x41 || buffer[offset+2] != 0x47) {
            return null;
        }
        ret.TitleInBytes = new byte[30];
        ret.ArtistInBytes = new byte[30];
        ret.AlbumInBytes = new byte[30];
        ret.YearInByte = new byte[4];
        Array.Copy(buffer, offset+ 3, ret.TitleInBytes,  0, 30);
        Array.Copy(buffer, offset+33, ret.ArtistInBytes, 0, 30);
        Array.Copy(buffer, offset+63, ret.AlbumInBytes,  0, 30);
        Array.Copy(buffer, offset+93, ret.YearInByte,    0,  4);
        if ( buffer[offset+125] == 0x00 ) { // Track情報あり
            ret.CommnetInByte = new byte[28];
            Array.Copy(buffer, offset+97, ret.CommnetInByte, 0, 28);
            ret.Track = buffer[offset+126];
        }
        else {
            ret.CommnetInByte = new byte[30];
            Array.Copy(buffer, offset+97, ret.CommnetInByte, 0, 30);
            ret.Track = 0;
        }
        ret.GenreNumber = buffer[offset+127];

        return ret;
    }
}

public class ID3v2
{
    const int CodePage_Shift_JIS = 932;


    static int SynchsafeIntFrom4Bytes(byte[] buffer, int offset) {
        return ((buffer[offset  ]&0x7F)<<21) |
               ((buffer[offset+1]&0x7F)<<14) | 
               ((buffer[offset+2]&0x7F)<< 7) | 
               ((buffer[offset+3]&0x7F)    ) ;
    }

    public class Frame
    {
        public string ID{get;private set;}
        int MajorVer;
        int Size;
        int EncodingID;
        byte Flags1;
        byte Flags2;
        byte[] Data; // EncodingIDを除く

        public override string ToString()
        {
            int offset = 0;

            if ( ID == "PIC" || ID == "APIC" ) {
                return "";
            }

            if ( ID == "COM" || ID == "COMM" ) {
                return ""; // 未実装
                /*
                if ( Data.Length >= 3+1 && 'a' <= Data[0] && Data[0] <= 'z' ) {
                    // 国別コードらしきものがある場合
                    offset = 3;
                    if ( EncodingID == 1 ) {
                        // BOM 2byteすてて 2byteずつサーチしてNULL終端0x00 00を探す
                    }
                    else if ( EncodingID == 2 ) {
                        // 2byteずつサーチしてNULL終端0x00 00を探す
                    }
                    else if ( EncodingID == 0 || EncodingID == 3 ) {
                        // NULL終端0x00を探す
                    }
                }
                else {
                    return ""; // 不正なフォーマット
                }
                */
            }

            try {
                if ( EncodingID == 3 ) {
                    // UTF-8 BOMなし
                    return (new System.Text.UTF8Encoding(false)).GetString(Data, offset, Data.Length-offset);
                }
                else if ( EncodingID == 2 ) {
                    // UTF-16BE BOMなし
                    return (new System.Text.UnicodeEncoding(true,false)).GetString(Data, offset, Data.Length-offset);
                }
                else if ( EncodingID == 1 ) {
                    // UTF-16 BOMあり
                    return (new System.Text.UnicodeEncoding()).GetString(Data, offset, Data.Length-offset);
                }
                else if ( EncodingID == 0 ) {
                    // MPEGの規格上は
                    //   ISO-8859-1 (CodePage=28591 西ヨーロッパ言語 (ISO))
                    //return Encoding.GetEncoding(28591).GetString(Data);

                    // 日本ではShift_JISが横行しているらしい(?)
                    //   shift_jis(CodePage=932)
                    return Encoding.GetEncoding(CodePage_Shift_JIS).GetString(Data, offset, Data.Length-offset);
                }
                else {
                    // unknown
                    return "";
                }
            }
            catch ( DecoderFallbackException ) {
                // 読み取り不能
                return "";
            }
        }

        public static Frame Parse(int majorVer, byte[] buffer, ref int pos, int endPos)
        {
            Frame ret = new Frame();

            if ( endPos > buffer.Length ){ return null; }

            if ( endPos < pos+6 ) { return null; }
            if ( majorVer >= 3 && endPos < pos+10 ) { return null; }

            ret.MajorVer = majorVer;
            try {
                ret.ID = Encoding.ASCII.GetString(buffer, pos, (majorVer<=2)?3:4 );
            }
            catch ( DecoderFallbackException ) {
                // 読み取り不能
                Console.WriteLine("Failed to parse at address 0x" + pos.ToString("X"));
                return null;
            }

            if ( majorVer <= 2 ) {
                ret.Size = (buffer[pos+3]<<16) | (buffer[pos+4]<<8) | (buffer[pos+5]);
                pos += 6;
            }
            else {
                if ( majorVer <= 3 ) {
                    ret.Size = (buffer[pos+4]<<24) |
                               (buffer[pos+5]<<16) | 
                               (buffer[pos+6]<<8) |
                               (buffer[pos+7]);
                }
                else {
                    ret.Size = SynchsafeIntFrom4Bytes(buffer, pos+4);
                }
                ret.Flags1 = buffer[pos+8];
                ret.Flags2 = buffer[pos+9];
                pos += 10;
            }
            if ( endPos < pos + ret.Size ) {
                Console.WriteLine("Failed to parse at address 0x" + pos.ToString("X"));
                Console.WriteLine("Address over");
                return null;
            }
            if ( ret.Size > 0 ) {
                ret.Data = new byte[ret.Size-1];
                ret.EncodingID = buffer[pos];
                Array.Copy(buffer, pos+1, ret.Data, 0, ret.Size-1);
            }
            else {
                ret.Data = new byte[0];
                ret.EncodingID = 0;
            }
            pos += ret.Size;

            return ret;
        }
    }

    public int TagVerMajor{get;private set;}
    public int TagVerMinor{get;private set;}
    public int Flags{get;private set;}
    public int TagSize{get;private set;}
    public int ExtSize{get;private set;}

    public bool HasExtendedHeader{get{return ((Flags&0x40)!=0);}}
    public bool HasFooter{get{return ((Flags&0x10)!=0);}}

    List<Frame> Frames;

    int FindFirstFrameByID(string FrameID) {
        for(int i=0;i<Frames.Count;i++){
            if ( Frames[i].ID == FrameID ) {
                return i;
            }
        }
        return -1;
    }

    public string Artist { get { return GetStringByID("TP1","TPE1"); } }
    public string Title { get { return GetStringByID("TT2","TIT2"); } }
    public string Album { get { return GetStringByID("TAL","TALB"); } }
    public string Track { get { return GetStringByID("TRK","TRCK"); } }
    public string Year { get { return GetStringByID("TYE","TYER"); } }
    public string Genre { get { return GetStringByID("TCO","TCON"); } }
    public string Comment { get { return GetStringByID("COM","COMM"); } }

    string GetStringByID(string idForV3p2, string idForV3p3)
    {
        string id = (TagVerMajor<=2)?idForV3p2:idForV3p3;
        if ( id == null ) { return ""; }
        int index = FindFirstFrameByID(id);
        if ( index < 0 ) { return ""; }
        return Frames[index].ToString();
    }

    public static ID3v2 Parge(byte[] buffer, int offset)
    {
        ID3v2 ret = new ID3v2();

        if ( offset < 0 ) {
            return null;
        }
        // ID3v2は最低でも10byte以上なので10byte以上であることをチェックする
        if (buffer.Length < offset + 10 ) {
            return null;
        }

        // ヘッダチェック "ID3"
        if ( buffer[offset] != 0x49 || buffer[offset+1] != 0x44 || buffer[offset+2] != 0x33) {
            return null;
        }

        ret.TagVerMajor = buffer[offset+3];
        ret.TagVerMinor = buffer[offset+4];
        ret.Flags = buffer[offset+5];
        ret.TagSize = SynchsafeIntFrom4Bytes(buffer, offset+6);

        int pos = offset+10;
        int endPos = pos + ret.TagSize;
        if ( endPos > buffer.Length ) {
            return null;
        }
        if ( ret.HasFooter ) {
            endPos -= 10; // Footer(10byte)分末尾位置を手前にセットする
        }

        if ( ret.HasExtendedHeader ) {
            // 最小6byteある
            if ( buffer.Length < pos+6 ) {
                return null;
            }
            if ( ret.TagVerMajor <= 3 ) {
                // IDv2.3.x以下
                ret.ExtSize = (buffer[pos  ]<<24) |
                              (buffer[pos+1]<<16) |
                              (buffer[pos+2]<< 8) |
                              (buffer[pos+3]    );
            }
            else {
                ret.ExtSize = SynchsafeIntFrom4Bytes(buffer, pos);
            }
            pos += 4 + ret.ExtSize;
        }

        // parsing Frame
        ret.Frames = new List<Frame>();
        while ( pos < endPos ) {
            if ( buffer[pos] == 0 ) { // padding領域(っぽい)を検出
                break;
            }
            Frame t = Frame.Parse(ret.TagVerMajor, buffer, ref pos, endPos);
            if ( t == null ) {
                Console.WriteLine("Failed to parse at address 0x" + pos.ToString("X"));
                return null;
            }
            else {
                ret.Frames.Add(t);
            }
        }
        return ret;
    }
}


class MainForm : Form
{
    ListView lsvID3v1;
    ListView lsvID3v2;

    MainForm(string filePath)
    {
        Text = "Mp3TagViewer";

        Controls.Add(
            lsvID3v1 = new ListView() {
                Location = new Point(0, 0),
                Size = new Size(600, 200),
                View = View.Details,
                FullRowSelect = true,
                GridLines = true,
                AllowDrop = true,
            }
        );
        lsvID3v1.Columns.Add("ID3v1項目", 150);
        lsvID3v1.Columns.Add("値", 250);
        lsvID3v1.DragEnter += Control_DragEnter;
        lsvID3v1.DragDrop += Control_DragDrop;

        Controls.Add(
            lsvID3v2 = new ListView() {
                Location = new Point(0, 200),
                Size = new Size(600, 400),
                View = View.Details,
                FullRowSelect = true,
                GridLines = true,
                AllowDrop = true,
            }
        );
        lsvID3v2.Columns.Add("ID3v2項目", 150);
        lsvID3v2.Columns.Add("値", 250);
        lsvID3v2.DragEnter += Control_DragEnter;
        lsvID3v2.DragDrop += Control_DragDrop;

        this.AllowDrop = true;
        this.DragEnter += Control_DragEnter;
        this.DragDrop += Control_DragDrop;


        if ( filePath != null ) {
            LoadFile(filePath);
        }

        ClientSize = new Size(600,650);
    }

    void Control_DragEnter(Object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
            e.Effect = DragDropEffects.Copy;
        }
        else {
            e.Effect = DragDropEffects.None;
        }
    }

    void Control_DragDrop(Object sender, DragEventArgs e)
    {
        var fileNames = (string[])e.Data.GetData(DataFormats.FileDrop, false);
        if ( fileNames != null && fileNames.Length == 1 ) {
            if ( fileNames[0].EndsWith(".mp3", true, null) ) {// Note: 第2引数はignoreCase
                LoadFile(fileNames[0]);
            }
        }
    }

    void LoadFile(string filePath)
    {
        if ( filePath.EndsWith(".mp3", true, null) ) {// Note: 第2引数はignoreCase
            byte[] data = File.ReadAllBytes(filePath);
            ID3v1 id3v1 = ID3v1.PargeFromFoot(data);
            ID3v2 id3v2 = ID3v2.Parge(data,0);

            RegisterID3v1ToControl(id3v1);
            RegisterID3v2ToControl(id3v2);
        }
    }


    void RegisterID3v1ToControl(ID3v1 id3v1)
    {
        lsvID3v1.Items.Clear();
        if ( id3v1 != null ) { 
            lsvID3v1.BeginUpdate();
            try {
                lsvID3v1.Items.AddRange(
                    new ListViewItem[]{
                        new ListViewItem(new string[]{"アーティスト", id3v1.Artist}),
                        new ListViewItem(new string[]{"アルバム", id3v1.Album}),
                        new ListViewItem(new string[]{"トラック", id3v1.Track.ToString()}),
                        new ListViewItem(new string[]{"曲名", id3v1.Title}),
                        new ListViewItem(new string[]{"年", id3v1.Year.ToString()}),
                        new ListViewItem(new string[]{"ジャンル", id3v1.Genre}),
                        new ListViewItem(new string[]{"コメント", id3v1.Comment}),
                    }
                );
            }
            finally {
                lsvID3v1.EndUpdate();
            }
        }
    }

    void RegisterID3v2ToControl(ID3v2 id3v2)
    {
        lsvID3v2.Items.Clear();
        if ( id3v2 != null ) { 
            lsvID3v2.BeginUpdate();
            try {
                lsvID3v2.Items.AddRange(
                    new ListViewItem[]{
                        new ListViewItem(new string[]{"アーティスト", id3v2.Artist}),
                        new ListViewItem(new string[]{"アルバム", id3v2.Album}),
                        new ListViewItem(new string[]{"トラック", id3v2.Track}),
                        new ListViewItem(new string[]{"曲名", id3v2.Title}),
                        new ListViewItem(new string[]{"年", id3v2.Year}),
                        new ListViewItem(new string[]{"ジャンル", id3v2.Genre}),
                        //new ListViewItem(new string[]{"コメント", id3v2.Comment}),
                    }
                );
            }
            finally {
                lsvID3v2.EndUpdate();
            }
        }
    }


    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new MainForm((args.Length==1)?args[0]:null));
    }
}

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

ASP.Net Core でスキャフォールディングを使用して、データベースから WebAPIを公開するまで

必要なツールをインストールする

1.Entity Framework Core ツールのインストール

以下のコマンドでインストーるします。
グローバルにインストールします。

dotnet tool install --global dotnet-ef

既にインストールしている場合は以下のコマンドで最新にします。

dotnet tool update --global dotnet-ef
dotnet ef


                     _/\__
               ---==/    \\
         ___  ___   |.    \|\
        | __|| __|  |  )   \\\
        | _| | _|   \_/ |  //|\\
        |___||_|       /   \\\/\\

Entity Framework Core .NET Command-line Tools 3.1.6

Usage: dotnet ef [options] [command]

Options:
  --version        Show version information
  -h|--help        Show help information
  -v|--verbose     Show verbose output.
  --no-color       Don't colorize output.
  --prefix-output  Prefix output with level.

Commands:
  database    Commands to manage the database.
  dbcontext   Commands to manage DbContext types.
  migrations  Commands to manage migrations.

Use "dotnet ef [command] --help" for more information about a command.

という表示がされれば成功です。

2.aspnet-codegenerator のインストール

以下のコマンドでインストールします。

dotnet tool install --global dotnet-aspnet-codegenerator

既にインストールしている場合は以下のコマンドで最新にします。

dotnet tool update --global dotnet-aspnet-codegenerator

nuget パッケージのインストール

以下のコマンドでそれぞれインストールします。

dotnet add package Microsoft.EntityframeworkCore.Design
dotnet add package Microsoft.EntityframeworkCore.Sqlite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityframeworkCore.Design

    この後出てくる dotnet ef コマンドでデータベースからモデルを作成する際に使用します。

  • Microsoft.EntityframeworkCore.Sqlite

    今回のプログラムはデータベースにSQLiteを使用します。

    他のDBMSを使用する場合はDBMSに合ったパッケージに変更するだけです。

  • Microsoft.VisualStudio.Web.CodeGeneration.Design

    この後出てくる dotnet aspnet-codegenerator コマンドでモデルからコントローラーを作成する際に使用します。

  • dotnet add package Microsoft.EntityFrameworkCore.SqlServer

    正直なぜ必要なのか分かりませんが、無いと dotnet aspnet-codegenerator コマンドでエラーになったため、インストールしました。

データベースからモデルの作成

以下のコマンドを実行します。

dotnet ef dbcontext scaffold "Data Source=wb.sqlite" Microsoft.EntityframeworkCore.Sqlite -o Models

-o オプションは出力ディレクトリを指定します。

モデルからコントローラーの作成

以下のコマンドを実行します。

dotnet aspnet-codegenerator controller -name wbController -async -api -m Wb -dc wbContext -outDir Controllers

-name コントローラーの名前

-async async
-api api
-m Wb モデルクラス
-dc DBコンテキスト
-outDir 出力ディレクトリ

Startup に DBコンテキストを追加する処理を記述

Startup.cs の ConfigureServices メソッドに以下を追記します。

startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddDbContext<Models.wbContext>(); //この行
        }

launchSettings.jsonの編集

launchSettings.json の launchUrl を今回作成した COntrollerにルーティングされるよう修正します。

具体的には launchUrl の部分です。

Properties/launchSettings.json
    "sample2": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/wb",
      "applicationUrl": "https://localhost:9999;http://localhost:9998",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }

以上で終了です。

実行するとブラウザで WebAPI より取得した json が表示されると思います。

最後に

今回使用したコマンとをまとめておきます。

apendix
dotnet tool update --global dotnet-ef
dotnet add package Microsoft.EntityframeworkCore.Design
dotnet add package Microsoft.EntityframeworkCore.Sqlite
dotnet ef dbcontext scaffold "Data Source=wb.sqlite" Microsoft.EntityframeworkCore.Sqlite -o Models
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet aspnet-codegenerator controller -name wbController -async -api -m Wb -dc wbContext -outDir Controllers
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.Net Core でスキャフォールディングを使用して、データベースから WebAPIを構築するまで

C#(ASP.Net Core) で dotnet-cli を使用して WebAPIを構築してみます。

プロジェクトを作成する

プロジェクトを新規に作成します。
以下のコマンドでプロジェクトの種類を検索してみます。

dotnet new --list
Templates                                         Short Name               Language          Tags
----------------------------------------------------------------------------------------------------------------------------------
Console Application                               console                  [C#], F#, VB      Common/Console
Class library                                     classlib                 [C#], F#, VB      Common/Library
WPF Application                                   wpf                      [C#]              Common/WPF
WPF Class library                                 wpflib                   [C#]              Common/WPF
WPF Custom Control Library                        wpfcustomcontrollib      [C#]              Common/WPF
WPF User Control Library                          wpfusercontrollib        [C#]              Common/WPF
Windows Forms (WinForms) Application              winforms                 [C#]              Common/WinForms
Windows Forms (WinForms) Class library            winformslib              [C#]              Common/WinForms
Worker Service                                    worker                   [C#]              Common/Worker/Web
Unit Test Project                                 mstest                   [C#], F#, VB      Test/MSTest
NUnit 3 Test Project                              nunit                    [C#], F#, VB      Test/NUnit
NUnit 3 Test Item                                 nunit-test               [C#], F#, VB      Test/NUnit
xUnit Test Project                                xunit                    [C#], F#, VB      Test/xUnit
Razor Component                                   razorcomponent           [C#]              Web/ASP.NET
Razor Page                                        page                     [C#]              Web/ASP.NET
MVC ViewImports                                   viewimports              [C#]              Web/ASP.NET
MVC ViewStart                                     viewstart                [C#]              Web/ASP.NET
Blazor Server App                                 blazorserver             [C#]              Web/Blazor
Blazor WebAssembly App                            blazorwasm               [C#]              Web/Blazor/WebAssembly
ASP.NET Core Empty                                web                      [C#], F#          Web/Empty
ASP.NET Core Web App (Model-View-Controller)      mvc                      [C#], F#          Web/MVC
ASP.NET Core Web App                              webapp                   [C#]              Web/MVC/Razor Pages
ASP.NET Core with Angular                         angular                  [C#]              Web/MVC/SPA
ASP.NET Core with React.js                        react                    [C#]              Web/MVC/SPA
ASP.NET Core with React.js and Redux              reactredux               [C#]              Web/MVC/SPA
Razor Class Library                               razorclasslib            [C#]              Web/Razor/Library/Razor Class Library
ASP.NET Core Web API                              webapi                   [C#], F#          Web/WebAPI
ASP.NET Core gRPC Service                         grpc                     [C#]              Web/gRPC
dotnet gitignore file                             gitignore                                  Config
global.json file                                  globaljson                                 Config
NuGet Config                                      nugetconfig                                Config
Dotnet local tool manifest file                   tool-manifest                              Config
Web Config                                        webconfig                                  Config
Solution File                                     sln                                        Solution
Protocol Buffer File                              proto                                      Web/gRPC

今回作成したい WebAPIのプロジェクトは WebAPIということが分かりましたので、以下のコマンドでプロジェクトを作成します。

dotnet new webapi --name wbapi

wb.sqlte という SQLite のファイルを元にスキャフォールディングしましたので、以降の内容はプロジェクトフォルダに wb.sqlte というファイルがある前提で記述しています。

必要なツールをインストールする

1.Entity Framework Core ツールのインストール

以下のコマンドでインストーるします。
グローバルにインストールします。

dotnet tool install --global dotnet-ef

既にインストールしている場合は以下のコマンドで最新にします。

dotnet tool update --global dotnet-ef
dotnet ef


                     _/\__
               ---==/    \\
         ___  ___   |.    \|\
        | __|| __|  |  )   \\\
        | _| | _|   \_/ |  //|\\
        |___||_|       /   \\\/\\

Entity Framework Core .NET Command-line Tools 3.1.6

Usage: dotnet ef [options] [command]

Options:
  --version        Show version information
  -h|--help        Show help information
  -v|--verbose     Show verbose output.
  --no-color       Don't colorize output.
  --prefix-output  Prefix output with level.

Commands:
  database    Commands to manage the database.
  dbcontext   Commands to manage DbContext types.
  migrations  Commands to manage migrations.

Use "dotnet ef [command] --help" for more information about a command.

という表示がされれば成功です。

2.aspnet-codegenerator のインストール

以下のコマンドでインストールします。

dotnet tool install --global dotnet-aspnet-codegenerator

既にインストールしている場合は以下のコマンドで最新にします。

dotnet tool update --global dotnet-aspnet-codegenerator

nuget パッケージのインストール

以下のコマンドでそれぞれインストールします。

dotnet add package Microsoft.EntityframeworkCore.Design
dotnet add package Microsoft.EntityframeworkCore.Sqlite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityframeworkCore.Design

    この後出てくる dotnet ef コマンドでデータベースからモデルを作成する際に使用します。

  • Microsoft.EntityframeworkCore.Sqlite

    今回のプログラムはデータベースにSQLiteを使用します。

    他のDBMSを使用する場合はDBMSに合ったパッケージに変更するだけです。

  • Microsoft.VisualStudio.Web.CodeGeneration.Design

    この後出てくる dotnet aspnet-codegenerator コマンドでモデルからコントローラーを作成する際に使用します。

  • dotnet add package Microsoft.EntityFrameworkCore.SqlServer

    正直なぜ必要なのか分かりませんが、無いと dotnet aspnet-codegenerator コマンドでエラーになったため、インストールしました。

データベースからモデルの作成

以下のコマンドを実行します。

dotnet ef dbcontext scaffold "Data Source=wb.sqlite" Microsoft.EntityframeworkCore.Sqlite -o Models

-o オプションは出力ディレクトリを指定します。

モデルからコントローラーの作成

以下のコマンドを実行します。

dotnet aspnet-codegenerator controller -name wbController -async -api -m Wb -dc wbContext -outDir Controllers

-name コントローラーの名前

-async async
-api api
-m Wb モデルクラス
-dc DBコンテキスト
-outDir 出力ディレクトリ

Startup に DBコンテキストを追加する処理を記述

Startup.cs の ConfigureServices メソッドに以下を追記します。

startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddDbContext<Models.wbContext>(); //この行
        }

launchSettings.jsonの編集

launchSettings.json の launchUrl を今回作成した COntrollerにルーティングされるよう修正します。

具体的には launchUrl の部分です。

Properties/launchSettings.json
    "sample2": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/wb",
      "applicationUrl": "https://localhost:9999;http://localhost:9998",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }

以上で終了です。

実行するとブラウザで WebAPI より取得した json が表示されると思います。

最後に

今回使用したコマンドをまとめておきます。

apendix
dotnet tool update --global dotnet-ef
dotnet add package Microsoft.EntityframeworkCore.Design
dotnet add package Microsoft.EntityframeworkCore.Sqlite
dotnet ef dbcontext scaffold "Data Source=wb.sqlite" Microsoft.EntityframeworkCore.Sqlite -o Models
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet aspnet-codegenerator controller -name wbController -async -api -m Wb -dc wbContext -outDir Controllers
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Microsoft] 番外. C#ソースコードとRazorの分離 - Angularチュートリアル Tour of Heroes を Blazor で再実装する

C#ソースコードとRazorの分離

今までRazorファイル中に@code {}としてC#プログラムを書いていました。

Page/Dashboard.razor
@page "/dashboard"
@using BlazorTourOfHeroes.Model
@using BlazorTourOfHeroes.Service
@inject IHeroService HeroService

<h3>Top Heroes</h3>
<div class="grid grid-pad">
    @foreach (var hero in heroes)
    {
      <NavLink href="@("detail/" + hero.Id)" class="col-1-4">
        <div class="module hero">
        <h4>@hero.Name</h4>
        </div>
      </NavLink>
    }
</div>

@code {
    private IEnumerable<Hero> heroes;

    protected override async Task OnInitializedAsync()
    {
        await GetHeroesAsync();
    }

    private async Task GetHeroesAsync()
    {
        heroes = (await HeroService.GetHeroes()).Take(5);
    }
}

このままでも問題ありませんが、長くなると見にくくなりますので、C#プログラムを別ファイルに分離します。

部分クラスとして別ファイルに分離する

C#プログラムをpartial classとして別ファイルにします。

ファイル名を決定する

ファイル名はコンポーネントにあわせてコンポーネント名.razor.csにします。

クラス名を決定する

クラス名はコンポーネントと同一にします。

Pages/Dashboard.razor.cs
public partial class Dashboard
{
}

名前空間を決定する

クラスの名前空間は以下のルールによって決まります。

  • Razorファイルに@namespaceがある場合はその名前
  • プロジェクトのルート名前空間 + razorファイルが存在するフォルダ名
Pages/Dashboard.razor.cs
namespace BlazorTourOfHeroes.Pages
{
    public partial class Dashboard
    {
    }
}

依存するクラスを注入する

コンポーネントの場合はプロパティインジェクションになります。1

Pages/Dashboard.razor.cs
[Inject]
private IHeroService HeroService { get; set; }

コードを移動する

Razorファイルにある@code {}の中身を新しく作成したC#ファイルに移動します。
必要なusing句を追加します。

Pages/Dashboard.razor.cs
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BlazorTourOfHeroes.Model;
using BlazorTourOfHeroes.Service;
using Microsoft.AspNetCore.Components;

namespace BlazorTourOfHeroes.Pages
{
    public partial class Dashboard
    {
        [Inject]
        private IHeroService HeroService { get; set; }
        private IEnumerable<Hero> heroes;

        protected override async Task OnInitializedAsync()
        {
            await GetHeroesAsync();
        }

        private async Task GetHeroesAsync()
        {
            heroes = (await HeroService.GetHeroes()).Take(5);
        }
    }
}

razorファイルを整理する

Razorファイルから不要になった@using@injectを削除します。

Pages/Dashboard.razor
@page "/dashboard"

<h3>Top Heroes</h3>
<div class="grid grid-pad">
    @foreach (var hero in heroes)
    {
      <NavLink href="@("detail/" + hero.Id)" class="col-1-4">
        <div class="module hero">
        <h4>@hero.Name</h4>
        </div>
      </NavLink>
    }
</div>

  1. コンポーネントではない場合はコンストラクタインジェクションになります。 

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

Observable.FromEventについて調べてみた

最近、ReactiveExtensionsを勉強しています。(前回の投稿からかなり飛んでいる。。。)
調べながら進めていますが、ファクトリメソッドEventFromがわかりにくい。
丸覚えでもいいのですが、もう少し理解したいと思い調べてみました。(今更の記事感がありますが)

Observable.FromEventについて

まずは基本。シグネチャを定義から引用します。

IObservable<TEventArgs> FromEvent<TDelegate, TEventArgs>(
Func<Action<TEventArgs>, TDelegate> conversion, Action<TDelegate> addHandler, 
Action<TDelegate> removeHandler);

わかりにくい。。(私だけ?)

定型のコール

Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
    h => (s, e) => h(e),
    h => button.Click += h,
    h => button.Click -= h
    ).Subscribe(e => textBlock.Text = string.Format("{0}: Ckicked",DateTime.Now));

ボタンクリックイベントを変換する処理。ボタンクリックされるとtextBlockのTextプロパティに日時を出力するサンプルです。

わかりにくい点

ここでわかりにくい点を挙げてみます。

  1. 第一引数は、=>が二つもあり複雑でわからない。
  2. サンプルすべての引数に「h」という変数が使われており、関連がかわらない。

わかりやすく書き換え

上記の点を踏まえて、わかりやすく書き換えてみます。

Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
    handler1 => 
    {
        RoutedEventHandler action = (sender, e) =>
        {
            handler1(e);
        };
        var conversion = new RoutedEventHandler(action);
        return conversion;
    },
    handler2 => button.Click += handler2,   // 第一引数の戻り値
    handler2 => button.Click -= handler2     // 第一引数の戻り値
    ).Subscribe(e => textBlock.Text = string.Format("{0}: Ckicked", DateTime.Now));

以下の点が見えてきました。

  1. 第一引数にある2つの=>は、handlerA(key)を返却する(return)処理
  2. 第一引数のhと、第二第三引数のhは別物
  3. そしてhandlerAとは、Subscribeに渡すメソッドのこと。

処理の流れ

処理の流れをまとめてみま。画面からボタンをクリックしたとき以下の処理が実行されます。

  1. KeyPressが発行される。
  2. handlerBが実行される。
  3. handlerBが参照しているAction「handlerA(key)」が実行される。
  4. handlerAとは、Subscribeの引数で渡されるメソッド(value => Console.WriteLine("OnNext({0})", value))

シグネチャを改めて見てみる

IObservable<TEventArgs> FromEvent<TDelegate, TEventArgs>(
Func<Action<TEventArgs>, TDelegate> conversion, Action<TDelegate> addHandler, 
Action<TDelegate> removeHandler);

第一型引数: TDelegate

  • 購読するイベントのシグネチャ
  • Rx世界への変換前の型(イベント)

第二型引数: TEventArgs

  • Observerへ渡る引数の型
  • Rx世界への変換後の型(ストリームに流す型)

まとめ

  • FromEventは.NETのイベントをRxの世界に流すことのできる素敵なファクトリメソッド。
  • 第一型引数には、イベントシグネチャを指定します。
  • 第二型引数には、ストリームに流すデータの型を指定します。

省略記法が多用されているので、わかる人にはわかりやすいのだろうけど初めての人にはとっつきにくいものですね。
(私の慣れが足りていないということかな)
最後に全コードを記載しておきます。

using System;
using System.Reactive.Linq;
using System.Windows;

namespace RxStudyWpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            //Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
            //    h => (s, e) => h(e),
            //    h => button.Click += h,
            //    h => button.Click -= h
            //    ).Subscribe(e => textBlock.Text = string.Format("{0}: Ckicked",DateTime.Now));


            Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
                handler1 => 
                {
                    RoutedEventHandler action = (sender, e) =>
                    {
                        handler1(e);
                    };
                    var conversion = new RoutedEventHandler(action);
                    return conversion;
                },
                handler2 => button.Click += handler2,
                handler2 => button.Click -= handler2
                ).Subscribe(e => textBlock.Text = string.Format("{0}: Ckicked", DateTime.Now));
        }
    }
}
<Window x:Class="RxStudyWpf.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"
        mc:Ignorable="d"
        Title="MainWindow" Height="150" Width="200">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Name="button" Content="Click"></Button>
        <TextBlock Grid.Row="1" Name="textBlock"></TextBlock>
    </Grid>
</Window>

参考:

[https://blog.xin9le.net/entry/2012/01/12/130607:embed:cite]

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

Visual Studioの基本機能で JSON からクラスを自動生成する方法

はじめに

あんまりこの辺の基本機能が有名ではないようなので紹介しておこうかと思います。
ちなみにですが XML でもまったく同じことが可能です。

前提

  • 適当な C# プロジェクトを作っておいてください

JSON

今回は以下のような JSON を利用していこうと思います。

参照: Qiita API v2 | /api/v2/templates/:template_id

{
  "body": "Weekly MTG on %{Year}/%{month}/%{day}",
  "id": 1,
  "name": "Weekly MTG",
  "expanded_body": "Weekly MTG on 2000/01/01",
  "expanded_tags": [
    {
      "name": "MTG/2000/01/01",
      "versions": [
        "0.0.1"
      ]
    }
  ],
  "expanded_title": "Weekly MTG on 2015/06/03",
  "tags": [
    {
      "name": "MTG/%{Year}/%{month}/%{day}",
      "versions": [
        "0.0.1"
      ]
    }
  ],
  "title": "Weekly MTG on %{Year}/%{month}/%{day}"
}

この JSON をクリップボードにコピーしておいてください。

Visual Studio

クラスファイルを 1つ 追加する

今回は Template.cs を追加しました。
image.png

JSON を クラス として貼り付ける

[ 編集 ][ 形式を選択して貼り付け ][ JSON をクラスとして貼り付ける ]
image.png

Rootobject クラスを修正する

Rootobject というクラスとともに関連クラスが自動で生成されるので, あとはお好みで修正していきます。
ここで注意しなければならないのは, "プロパティ名を変更してはならない" ということです。クラス名は変更しても問題ありません。

今回は以下のようにしました。
image.png

ここで問題になってくるのが, プロパティ名が通常の C# コーディングガイドラインに則っていないことです。
そこで今回は System.Runtime.Serialization.DataMemberAttribute を利用していこうと思います。

各プロパティにこの属性を利用してあげて、以下のようにします。
image.png

これで Upper Camel Case でプロパティを書きつつ正常にデシリアライズできるようになりました。
ここで注意ですが, 利用するデシリアライザによっては別の Attribute が用意されている可能性があるので, その辺りはドキュメントに従って良しなに対応してください。

デシリアライズしてみる

nugetから Utf8Json を導入してデシリアライズしてみます。

Program.cs
using System;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var json = @""
{
  ""body"": ""Weekly MTG on %{Year}/%{month}/%{day}"",
  ""id"": 1,
  ""name"": ""Weekly MTG"",
  ""expanded_body"": ""Weekly MTG on 2000/01/01"",
  ""expanded_tags"": [
    {
      ""name"": ""MTG/2000/01/01"",
      ""versions"": [
        ""0.0.1""
      ]
    }
  ],
  ""expanded_title"": ""Weekly MTG on 2015/06/03"",
  ""tags"": [
    {
      ""name"": ""MTG/%{Year}/%{month}/%{day}"",
      ""versions"": [
        ""0.0.1""
      ]
    }
  ],
  ""title"": ""Weekly MTG on %{Year}/%{month}/%{day}""
}
            "";

            var x = Utf8Json.JsonSerializer.Deserialize<Template>(json);
            Console.WriteLine($"body= {x.Body}\r\nid= {x.ID}\r\nname= {x.Name}");
        }
    }
}

実行結果は以下のようになります。
image.png

おわりに

生成されるクラスが最初から Upper Camel だと, もっと使いやすいのですが…。
そこは今後に期待というところですね。

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

[C#]JSONデシリアライズ化に便利なサイトの紹介と注意事項

主題

JSONファイルからデシリアライズ後のデータクラス定義を自動生成してくれるサイトの紹介と、
実際に使ってみた際に気づいた点を説明。

json2csharp.com

https://json2csharp.com/

利用手順

1.JSONファイルの内容をコピペする。
2.Convertをクリックすると、C#のクラス宣言コードが生成される。

なお、JSONファイル内でキー名が定義されていない要素は、"Root"や”MyArray”などの適当な名称が割り振られるため、
適切な名称にリネームする必要がある。
スクリーンショット 2020-08-02 11.31.18.png

注意事項

Convert後の先頭行にはJSON.NETライブラリによるデシリアライズするサンプルコードも一緒に出力されるが、
これは正常に動作しない場合がある。

上手く行かない例

[
    {
        "frames": [
            {
                "Delay": 0,
                "Index": 0
            }
        ],
        "Name": ""
    },
    {
        "frames": [
            {
                "Delay": 0,
                "Index": 3
            }
        ],
        "Name": ""
    }
]

上記のように、JSONの最上位要素がコレクションや配列になっている場合、当該サイトでコンバートすると、以下の結果となる。

    Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse); //デシリアライズコード
    public class Frame    {
        public int Delay { get; set; } 
        public int Index { get; set; } 
    }

    public class MyArray    {
        public List<Frame> frames { get; set; } 
        public string Name { get; set; } 
    }

    public class Root    {
        public List<MyArray> MyArray { get; set; } 
    }

一見すると、デシリアライズが上手く行そうだが、実際に実行すると以下例外発生する。

JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Root' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.

エラー内容を見ると、配列オブジェクトを単一オブジェクトでデシリアライズしようとしたので怒られている様子。
JSONファイルの構造を変えない場合は、以下のコードでデシリアライズ可能。

List<MyArray> root = JsonConvert.DeserializeObject<List<MyArray>>(json);

上手く行く例

{
    "MyArray": [
        {
            "frames": [
                {
                    "Delay": 0,
                    "Index": 0
                }
            ],
            "Name": ""
        },
        {
            "frames": [
                {
                    "Delay": 0,
                    "Index": 3
                }
            ],
            "Name": ""
        }
    ]
}

先ほどと違い、JSONの最上位要素が{}で括られている、つまり単一オブジェクトの場合は先ほどの例と同じコンバート結果になり、
デシリアライズもサンプルコードのままで上手くいく。

    Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse); //デシリアライズコード
    Console.WriteLine("Count="+ myDeserializedClass.MyArray.Count.ToString());

    public class Frame    {
        public int Delay { get; set; } 
        public int Index { get; set; } 
    }

    public class MyArray    {
        public List<Frame> frames { get; set; } 
        public string Name { get; set; } 
    }

    public class Root    {
        public List<MyArray> MyArray { get; set; } 
    }
Count=2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 後編

この記事は 2020 年の ReactiveProperty のオーバービューの全 3 編からなる記事の 3 つ目の記事です。

他の記事はこちらです。

イベントから ReactiveProperty や ReactiveCommand を呼ぶ

WPF と UWP 限定の機能としてボタンのクリックなどのイベントが発生したら ReactiveProperty の値を更新したり、ReactiveCommand を呼び出すといった機能を提供しています。EventToReactiveProperty と EventToReactiveCommand を使用して、この機能が利用可能です。

この機能を利用するにはプラットフォーム固有のパッケージをインストールする必要があります。

  • ReactiveProperty.WPF (WPF 用)
  • ReactiveProperty.UWP (UWP 用)

上記パッケージをインストールすると Microsoft.Xaml.Behaviors.Wpf (WPF 用)、
Microsoft.Xaml.Behaviors.Uwp.Managed (UWP 用) パッケージもインストールされます。このパッケージ内にある EventTrigger と EventToReactiveProprety/EventToReactiveCommand を組み合わせて使うことでイベントをハンドリングして ReactiveProperty/ReactiveCommand に伝搬することが出来ます。

また、イベント発生時のイベント引数を変換するための変換レイヤーも提供しています。DelegateConverter<T, U>ReactiveConverter<T, U> を継承して作成します。型引数の T が変換元(普通は XxxEventArgs)で U が変換先 (ReactiveProperty の値の型やコマンドパラメーターの型) です。

例えば WPF でマウスを動かしたときのイベント引数の MouseEventArgs を表示用メッセージに加工するコンバーターは以下のようになります。

MouseEventToStringConverter.cs
using Reactive.Bindings.Interactivity;
using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Input;

namespace RxPropLabWpf
{
    public class MouseEventToStringReactiveConverter : ReactiveConverter<MouseEventArgs, string>
    {
        protected override IObservable<string> OnConvert(IObservable<MouseEventArgs> source) =>
            source
                // MouseEventArgs から GetPosition でマウスポインターの位置を取得(AssociateObject で EventTrigger を設定している要素が取得できる)
                .Select(x => x.GetPosition(AssociateObject as IInputElement))
                // ReactiveProperty に設定する文字列に加工
                .Select(x => $"({x.X}, {x.Y})");
    }

    public class MouseEventToStringDelegateConverter : DelegateConverter<MouseEventArgs, string>
    {
        protected override string OnConvert(MouseEventArgs source)
        {
            // MouseEventArgs から ReactiveProperty に設定する文字列に加工
            var pos = source.GetPosition(AssociateObject as IInputElement);
            return $"({pos.X}, {pos.Y})";
        }
    }
}

2 つのクラスは同じ処理をしています。ReactiveConverter は変換処理を Rx のメソッドチェーンで書けます。DelegateConverter は変換処理を普通の C# のメソッドとして書けます。
このコンバーターを使って View のイベントを ReactiveProperty や ReactiveCommand に伝搬させる先の ViewModel を作成します。今回は確認ようにシンプルに受け取ったメッセージを格納するための ReactiveProperty と、ReactiveCommand を用意しました。

using Reactive.Bindings;
using System.ComponentModel;
using System.Reactive.Linq;

namespace RxPropLabWpf
{
    // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactivePropertySlim<string> Message { get; }
        public ReactiveCommand<string> CommandFromViewEvents { get; }
        public ReadOnlyReactivePropertySlim<string> MessageFromCommand { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();
            CommandFromViewEvents = new ReactiveCommand<string>();
            MessageFromCommand = CommandFromViewEvents.Select(x => $"Command: {x}")
                .ToReadOnlyReactivePropertySlim();
        }
    }
}

ReactiveCommand は実行されると受け取った文字を加工して MessageFromCommand という名前の ReadOnlyReactiveProperty に流しています。これを XAML にバインドします。EventToReactiveProperty と EventToReactiveCommand は EventTrigger の子要素として配置します。そして EventToReactiveProperty と EventToReactiveCommand の子要素としてコンバーターを指定します。

<Window x:Class="RxPropLabWpf.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:RxPropLabWpf"
        xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:rp="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <!-- ViewModel を設定して -->
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <TextBlock Text="ToReactiveProperty" />
        <Border Grid.Row="1"
                Background="Blue"
                Margin="10">
            <!-- MouseMove イベントを MouseEventToStringReactiveConverter で変換して ReactiveProperty に設定する -->
            <behaviors:Interaction.Triggers>
                <behaviors:EventTrigger EventName="MouseMove">
                    <rp:EventToReactiveProperty ReactiveProperty="{Binding Message}">
                        <local:MouseEventToStringReactiveConverter />
                    </rp:EventToReactiveProperty>
                </behaviors:EventTrigger>
            </behaviors:Interaction.Triggers>
            <TextBlock Text="{Binding Message.Value}" Foreground="White" />
        </Border>

        <TextBlock Text="ToReactiveCommand" 
                   Grid.Column="1" />
        <Border Grid.Row="1"
                Grid.Column="1"
                Background="Red"
                Margin="10">
            <!-- MouseMove イベントを MouseEventToStringReactiveConverter で変換して ReactiveCommand を実行する -->
            <behaviors:Interaction.Triggers>
                <behaviors:EventTrigger EventName="MouseMove">
                    <rp:EventToReactiveCommand Command="{Binding CommandFromViewEvents}">
                        <local:MouseEventToStringReactiveConverter />
                    </rp:EventToReactiveCommand>
                </behaviors:EventTrigger>
            </behaviors:Interaction.Triggers>
            <TextBlock Text="{Binding MessageFromCommand.Value}" Foreground="White" />
        </Border>
    </Grid>
</Window>

実行すると以下のようになります。EventToReactiveProperty はコンバーターで変換した結果がそのまま表示されています。EventToReactiveCommand のほうは、コマンドで加工したメッセージが表示されていることが確認できます。

rpc.gif

Notifiers

Reactive.Bindings.Notifiers 名前空間には、いくつかの IObservable を拡張したクラスがあります。

  • BooleanNotifier
  • CountNotifier
  • ScheduledNotifier
  • BusyNotifier
  • MessageBroker
  • AsyncMessageBroker

単品で見ると大したこと無いクラスですが、これらも IObservable なので ReactiveProperty や RreactiveCommand や ReactiveCollection とつないで使うことが出来ます。
とはいっても使用頻度は少なめなので、Notifier 関連の詳細はドキュメントを参照してください。

Notifiers | ReactiveProperty document

ここでは MessageBroker と AsyncMessageBroker を紹介します。

MessageBroker / AsyncMessageBroker

この 2 つのクラスはグローバルにメッセージを配信して購読するための機能を提供します。Prism でいう IEventAggregator が近い機能を提供しています。他にはメッセンジャー パターンなどと言われている機能を Rx フレンドリーに実装したものになります。

MessageBroker と AsyncMessageBroker は MessageBroker.DefaultAsyncMessageBroker.Default でシングルトンのインスタンスを取得できます。ただ、これはグローバルにメッセージを配信するユースケースが多いので利便性のために提供しているもので独自に new を使ってインスタンスを生成して使うことも可能です。

MessageBroker は ToObservable を呼ぶことで IObservable に変換できます。AsyncMessageBroker クラスは非同期処理に対応しています。AsyncMessageBroker は IObservable には変換できません。
使用方法を以下に示します。

using Reactive.Bindings.Notifiers;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;

public class MyClass
{
    public int MyProperty { get; set; }

    public override string ToString()
    {
        return "MP:" + MyProperty;
    }
}
class Program
{
    static void RunMessageBroker()
    {
        // global scope pub-sub messaging
        MessageBroker.Default.Subscribe<MyClass>(x =>
        {
            Console.WriteLine("A:" + x);
        });

        var d = MessageBroker.Default.Subscribe<MyClass>(x =>
        {
            Console.WriteLine("B:" + x);
        });

        // support convert to IObservable<T>
        MessageBroker.Default.ToObservable<MyClass>().Subscribe(x =>
        {
            Console.WriteLine("C:" + x);
        });

        MessageBroker.Default.Publish(new MyClass { MyProperty = 100 });
        MessageBroker.Default.Publish(new MyClass { MyProperty = 200 });
        MessageBroker.Default.Publish(new MyClass { MyProperty = 300 });

        d.Dispose(); // unsubscribe
        MessageBroker.Default.Publish(new MyClass { MyProperty = 400 });
    }

    static async Task RunAsyncMessageBroker()
    {
        // asynchronous message pub-sub
        AsyncMessageBroker.Default.Subscribe<MyClass>(async x =>
        {
            Console.WriteLine($"{DateTime.Now} A:" + x);
            await Task.Delay(TimeSpan.FromSeconds(1));
        });

        var d = AsyncMessageBroker.Default.Subscribe<MyClass>(async x =>
        {
            Console.WriteLine($"{DateTime.Now} B:" + x);
            await Task.Delay(TimeSpan.FromSeconds(2));
        });

        // await all subscriber complete
        await AsyncMessageBroker.Default.PublishAsync(new MyClass { MyProperty = 100 });
        await AsyncMessageBroker.Default.PublishAsync(new MyClass { MyProperty = 200 });
        await AsyncMessageBroker.Default.PublishAsync(new MyClass { MyProperty = 300 });

        d.Dispose(); // unsubscribe
        await AsyncMessageBroker.Default.PublishAsync(new MyClass { MyProperty = 400 });
    }

    static void Main(string[] args)
    {
        Console.WriteLine("MessageBroker");
        RunMessageBroker();

        Console.WriteLine("AsyncMessageBroker");
        RunAsyncMessageBroker().Wait();
    }
}

実行結果は以下のようになります。

MessageBroker
A:MP:100
B:MP:100
C:MP:100
A:MP:200
B:MP:200
C:MP:200
A:MP:300
B:MP:300
C:MP:300
A:MP:400
C:MP:400
AsyncMessageBroker
2020/08/02 10:59:39 A:MP:100
2020/08/02 10:59:39 B:MP:100
2020/08/02 10:59:41 A:MP:200
2020/08/02 10:59:41 B:MP:200
2020/08/02 10:59:43 A:MP:300
2020/08/02 10:59:43 B:MP:300
2020/08/02 10:59:45 A:MP:400

AsyncMessageBroker のほうは、2 秒ごとにログが出ているので await 出来ていることが確認できます。

各種拡張メソッド

IObservable 向けの便利な拡張メソッドをいくつか用意しています。使う場合は Reactive.Bindings.Extensions 名前空間を using してください。

ここでは特に使用頻度が高いと思うものだけを紹介します。完全なリストは以下のドキュメントを参照してください。

Extension methods | ReactiveProperty document

CombineLatestValuesAreAllTrue/CombineLatestValuesAreAllFalse

IEnumerable<IObservable<bool>> に対して最後の値がすべて true かどうか、もしくは false かどうかを表す bool を後続に流す IObservable<bool> に変換します。
例えば複数の ReactiveProperty の ObserveHasErros が全て false (エラーなし) になったら実行できるコマンドの生成などで便利です。以下のようになります。

// rp1, rp2, rp3 は ReactiveProperty 
SomeCommand = new[] // ReactiveProperty の ObserveHasErrors が
  {
    rp1.ObserveHasErrors,
    rp2.ObserveHasErrors,
    rp3.ObserveHasErrors,
  }
  .CombineLatestValuesAreAllFalse() // 全て false の場合に
  .ToReactiveCommand(); // 実行可能なコマンド

ObserveElementProperty

ObservableCollection<T> の型引数 T が INotifyPropertyChanged の場合に利用できる拡張メソッドです。ObservableCollection<T> の全ての要素の PropertyChanged イベントを監視できます。

コード例を以下に示します。

using Reactive.Bindings.Extensions;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace ReactivePropertyEduApp
{
    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var c = new ObservableCollection<Person>();
            c.ObserveElementProperty(x => x.Name)
                .Subscribe(x => Console.WriteLine($"Subscribe: {x.Instance}, {x.Property.Name}, {x.Value}"));

            var neuecc = new Person { Name = "neuecc" };
            var xin9le = new Person { Name = "xin9le" };
            var okazuki = new Person { Name = "okazuki" };

            Console.WriteLine("Add items");
            c.Add(neuecc);
            c.Add(xin9le);
            c.Add(okazuki);

            Console.WriteLine("Change okazuki name to Kazuki Ota");
            okazuki.Name = "Kazuki Ota";

            Console.WriteLine("Remove okazuki from collection");
            c.Remove(okazuki);

            Console.WriteLine("Change okazuki name to okazuki");
            okazuki.Name = "okazuki";
        }
    }
}

実行すると以下のようになります。

Add items
Subscribe: ReactivePropertyEduApp.Person, Name, neuecc
Subscribe: ReactivePropertyEduApp.Person, Name, xin9le
Subscribe: ReactivePropertyEduApp.Person, Name, okazuki
Change okazuki name to Kazuki Ota
Subscribe: ReactivePropertyEduApp.Person, Name, Kazuki Ota
Remove okazuki from collection
Change okazuki name to okazuki

コレクションにある要素のプロパティの変更が監視できていることがわかります。またコレクションから削除した要素(この場合は okazuki 変数)は削除後は変更してもコールバックが呼ばれていないことも確認できます。

コレクションの要素が POCO ではなく、ReactiveProperty を持つクラスの場合も ObserveElementObservableProperty 拡張メソッドを使うとコレクション内のオブジェクトの ReactiveProperty の監視を行えます。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace ReactivePropertyEduApp
{
    public class Person
    {
        public ReactiveProperty<string> Name { get; }

        public Person(string name)
        {
            Name = new ReactiveProperty<string>(name);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var c = new ObservableCollection<Person>();
            c.ObserveElementObservableProperty(x => x.Name)
                .Subscribe(x => Console.WriteLine($"Subscribe: {x.Instance}, {x.Property.Name}, {x.Value}"));

            var neuecc = new Person("neuecc");
            var xin9le = new Person("xin9le");
            var okazuki = new Person("okazuki");

            Console.WriteLine("Add items");
            c.Add(neuecc);
            c.Add(xin9le);
            c.Add(okazuki);

            Console.WriteLine("Change okazuki name to Kazuki Ota");
            okazuki.Name.Value = "Kazuki Ota";

            Console.WriteLine("Remove okazuki from collection");
            c.Remove(okazuki);

            Console.WriteLine("Change okazuki name to okazuki");
            okazuki.Name.Value = "okazuki";
        }
    }
}

実行すると以下のようになります。

Add items
Subscribe: ReactivePropertyEduApp.Person, Name, neuecc
Subscribe: ReactivePropertyEduApp.Person, Name, xin9le
Subscribe: ReactivePropertyEduApp.Person, Name, okazuki
Change okazuki name to Kazuki Ota
Subscribe: ReactivePropertyEduApp.Person, Name, Kazuki Ota
Remove okazuki from collection
Change okazuki name to okazuki

IObservable<bool> の反転

Inverse 拡張メソッドを使うと ox.Select(x => !x)ox.Inverse() のように書けます。それだけ。

await と使いたい

ReactiveProperty は Rx の機能を使ってメソッドチェーンが綺麗にきまると気持ちいいですが、やりすぎると可読性の低下や、知らない人にはトリッキーなコードになってしまうといった問題があります。
ReactiveProperty や async/await にも対応しているので、そちらを使って値の変更があったタイミングで処理を書くということも出来ます。ただ、現状まだちょっと非力です。

値が変わるまで await したい

WaitUntilValueChangedAsync メソッドで await で待つことが出来ます。コード例を以下に示します。

using Reactive.Bindings;
using Reactive.Bindings.Notifiers;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        var rp = new ReactivePropertySlim<string>();

        _ = WaitAndOutputAsync(rp);

        rp.Value = "Hello world";
    }

    static async ValueTask WaitAndOutputAsync(IReactiveProperty<string> rp)
    {
        var value = await rp.WaitUntilValueChangedAsync();
        Console.WriteLine($"await してゲットした値: {value}");
    }
}

実行すると以下のような結果になります。

await してゲットした値: Hello world

コマンドも同様に await が可能です。

まとめ

ということで前編・中編・後編終わりました。
適当機能強化とかがあったら、ここを更新していこうと思います。

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

【習い事レビュー】TECH ACADEMY Unityコース - テックアカデミー ・ユニティーコース-

2019年4月=2019年6月まで、
【TECH ACADEMY Unityコース(三カ月)】(約¥300,000)に関して、
実際私が体験したうえでのレビューを綴ります。
受講を検討されていらっしゃる方のご参考になればと思います。

評価:△
『メンターは良いが……果たしてそれでコスパに見合うのか?』

コースの概要
TECH ACADEMY Unityコース(以後、テックアカデミー またはユニティコース)は、
ゲームエンジン『Unity』に関して短期間・オンラインにて学習を完了させるコースです。
専門学校、専修学校とは違い、履歴書に『大卒』『専門学校卒』という肩書がつくものではありません。
おそらく一般的な学割も適応されません。
(ただし、Adobe Creative Cloud のような開発に必須なツールやアプリは安く買えるかも?)

受講の形態は、まず、専用ウェブサイトに記載されたテキストを学習します。
授業動画視聴ではなく、あくまでもテキストです。これらをこなすのに、一日最低二時間ずつ学習してもひと月半はかかる見込みです。
次に、各トピックの最後に製作課題が与えられ、実際に自らUnityで作ったプロジェクト(編集中のアプリのようなもの)を提出します。
添削して頂けるのは『メンター』と呼ばれる、いわゆる指導役、先生です。
メンターは現役のエンジニアが副業として請け負っている場合が多いようです。
現役の視点で製作物の講評をしてくれます。
トピックは確か10項目。製作物は6つあったかと思います。

コースの特徴
他の法人のオンラインコースを満遍なく受講したわけではありませんが、テックアカデミー独特のコースの特徴は、
・週二回のメンタリング制度
・オリジナルアプリのリリースがゴール
・選べる受講期間
・転職サービス『TECH ACADEMY キャリア』の利用
という点です。

・メンタリング制度について
前述したとおり、メンターは受講者の製作物の講評をしてくださる方です。現役エンジニアの方が多いです。
それに加えて、テックアカデミーでは週に二回、ビデオ通話による【メンタリング】があります。
スカイプのようにリアルタイムで対面して会話を行うことです。
時間は30分です。
いつでも受け付けられてるわけではなく、受講当初に曜日、時間帯を指定し、以後それが固定されます。
注意すべきは、ドタキャン二回で以後、メンタリングを受け付けてもらえなくなる恐れがあることです。
キャンセルは遅くとも前日の内に行うようにしましょう。

このメンタリング制度はすこぶる使えます。
メンターにより当たりはずれはあるでしょうが、私が担当してくださった30歳前後の先生はとても丁寧なご指導をして下さる方でした。
プログラミングに関する質問の他、後述するオリジナルアプリに搭載する機能を相談する際はテックアカデミーの学習外の内容にも関わらず、
またゲーム業界(Unityはゲームエンジンなので、就職先の第一候補がゲーム業界になると思います)
の内情をお尋ねした際も、嫌がらずにお答えいただきました。
(尋ね方にもよるかもしれません)
あなたがどれほど積極的に、または尋ね上手な人であるかによって、テックアカデミーの受講内容以上の成果をこのメンタリングによって得られるかもしれません。
この制度に受講料の大半の価値があるといっても良いでしょう。
(後述しますが、それだけテキスト教材がクソ一歩手前だという裏返しです)

・オリジナルアプリのリリースがゴール
本コースの最終的な目標、課題が『アップルストア、もしくはGoogle Playへのアプリのリリース』となっています。
つまり、スマホアプリとして公開するという事です。
その為の学習基礎と、公開の方法は学習にて身につきます。
私見ですが、Google Play の方が敷居が低くて失敗しにくいです。
最悪、Unityroomなどの無料ゲーム投稿サイトへの投稿でも、可としてくれる場合もあるそうです。
オリジナルアプリをリリースしたという実績は、未経験の方が転職活動の際にある程度優位性があるはずです。(それでも苦労していますが)
企業側も、いまいちどんな会社で何カ月勉強しただけという分かりにくい実績よりも、具体的な製作物を見ていただく方が分かりやすいでしょう。

・選べる受講期間
テックアカデミーでは同じコースであっても人によって受講期間をどれだけ取るか選択できる幅があります。
Unityコースでは、最短で4週間、最長で16週間です。
それによって授業料もかなり変動します。2020年夏現在は、社会人であれば15万円~30万円と二倍のふり幅があります。
ただし、時間からの単価でみれば、長い方が安上がりです。
4週間のプランでは、毎日8時間以上学習してもやり切るのは厳しいかもしれません。
オリジナルアプリの製作にも時間がかかります。
個人的には、就業中の方であれば12週程度をおすすめします。

・転職サイト『TECH ACADEMYキャリア』の利用
テックアカデミーでは専用の転職サイトを持っているようです。
私はあまり活用できていないので、多くは語れませんが、傾向としてはウェブ系・都市部・契約または正社員の求人が多い印象です。

・良い点。ダメな点
 
〇前述したとおり、メンターとメンタリングの制度は充分価値があります。このコースの全てといえるかもしれません。おそらく他のオンライン学習サービスの中でも比較的質は良いと思います。(確約しませんが)
 
〇オリジナルアプリリリースというゴールも、分かりやすく具体的です。もしあなたがそこまで作りこめる自信が無くても、学習を進める内に「こんなアプリを作りたい」と思えるかもしれませんし、最悪、提出した成果物を少しいじった程度の物でも合格できます。本当の最悪は提出できないことで、受講者の半数は提出すらできないそうです。
 ちなみにオリジナルアプリはテックアカデミーのコンテストに投稿できます。おそらく学習を終えた後でも投稿できるので、受講中に受賞することは非常に難しいと思いますが、受賞すれば箔がつくと思います。

△転職サービスは活用しづらい
 というのが私の本音です。というのも、そもそもUnityコースは求人が少なく、限定的でした。業界の傾向ではありますが、都市部限定なので地方出身者からしたらハードルが上がり、求人ラインナップもイマイチな会社が多いので、思い切って飛び込む勇気が持てませんでした。転職を諦めたというわけではなく、まだ力不足と感じたので、別の学習サイト、求人サービスを利用しながらチャンスを狙っています。

△充分な実力がついたとは言い難い
 後述するテキスト教材にも関わりますが、三カ月程度で学習を終えるレベルなので、他の2~4年かけて学習してきた新卒の専門学校生、大学生と比べると力量が見劣りしてしまいます。競争も厳しいでしょう。思い切って転職活動して上手くいくか、またはこのコースを第一歩として、更に学習を続けるか、それとも諦めるかという進路になるかと思います。

×テキスト教材が詐欺まがい
 これには正直がっかりしました。市販されている三千円程度の実用書と大差ない内容です。明らかに料金に見合いません。
 独自の内容と言えば、GitHub、Slackの活用と設定方法。またアプリのリリースの方法でしょうか。
 GitHubは今でも重宝していますが、しかしアプリのリリース方法の解説は、正直不満です。
 リリース直前になってアセットバンドルやら上手くビルドできない不具合が起きても、センターに尋ねる以外のサポートはありません。そのメンタリングも、もうコースの終盤ですのであと2,3回しかないという場合になりがちです。
 説明を端折っているところも見受けられ、Androidでのビルドにおいて、特定の機能を加えなくてはいけないのにその記述がないなど、詰めが甘く感じました。
 また、教材自体が少し古いと感じました。今では改訂されているのでしょうか?

×メンターにおしつけすぎじゃないか?
 前述した通り、このコースの価値の大半はメンターにあるのに、テックアカデミーという会社自体からは後にも先にも具体的なサポートやケアを受けた記憶がありません。営業メールが時々来るぐらいでしょうか? 受講中も簡単なメールのやりとりを数回しただけです。悪い見方をすれば、お金貰ったら後は管理だけといった具合でしょうか。
 受講者の方の中には、質問下手だったり人見知りしたり、たまたまメンターが悪かったりで、メンタリングで充分な恩恵を受けられない可能性もあります。そういった方にはお金をドブに捨てるような講座です。
 反面、このコースをすぐにではないにしろしっかりと活かし、目標を達成する受講者もいらっしゃることでしょう。
 すべてはやる気と運と地頭といったところです。

まとめ
〇 質の良いメンターと週二回のメンタリング
〇 オリジナルアプリリリースという具体的な実績を伴うゴール
△ 転職サービスがイマイチ
✕ テキスト教材はクソ
✕ テックアカデミー自体からのサポートが薄い

結論
『『メンターは良いが……果たしてそれでコスパに見合うのか?』
評価は『△』

と私は結論づけます。
もっと安く学習するなら『Udemy』や『LinkedIn』でも充分かもしれません。
もっと箔をつけたいのなら、『デジタルハリウッド』や、あなたがお若ければ専門学校に入り直したり、夜間や通信コースを取る方がベターかもしれません。
しかし私にとっては、紛れもなくゲーム開発者の第一歩を踏み出せたキッカケであり、学習から一年経ち、今なおゲーム開発を学習し続けても成果が上がっているとは言えませんが、思い切って受講してみて良かったといえるコースだったかなと思います。

最後までお目通しいただきありがとうございました。
ちなみにこのコースを受講した際に私がリリースしたアプリが↓
『Smile Me Baby』
https://play.google.com/store/apps/details?id=com.Company.SmileMeBaby&hl=ja
https://unityroom.com/games/smile_me_baby

最近リリースしたものがこちらです↓
『トレジャークエスト』
https://play.google.com/store/apps/details?id=com.Kabakamon.TreasureQuest&hl=ja
https://unityroom.com/games/treasurequest

もしよろしければお試しいただければと思います。
以上、ありがとうございました。
 

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