vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

MacでClosedXMLを使ってExcel開こうとしたらエラーになった話

はじめに

最近戯れに Teratail の C# に関する質問で、答えられそうなものを答えてみる、ということをしています。

今回は↓を解決したかったのですが、

teratail.com

何故か別の問題がでてしまい、それを解決したときのメモです。

環境

発生した問題

まず以下のようなコードを実行しようとしました。
(なお Windows 環境では問題なく動作することは確認しています)

Program.cs

class Program {
    private const string FilePath = @"HelloWorld.xlsx";
    private const string SheetName = "Sample Sheet";

    static void Main(string[] args) {
        using (var workbook = GetWorkbook()) {
            using (var worksheet = GetWorksheet(workbook)) {
                for (var i = 1; i <= 50; i++)
                {
                    // 空のセルが見つかるまでスキップし、値を入れたらそこで処理を終了.
                    if (worksheet.Cell("A" + i).Value.ToString() == "") {
                        worksheet.Cell("A" + i).Value = "Hello world";
                        break;
                    }
                }
            }
            workbook.Save();
        }
    }

    static XLWorkbook GetWorkbook() {
        if (File.Exists(FilePath)) {
            return new XLWorkbook(FilePath);
        }
        // ファイルが存在しなければ新規作成.
        var workbook = new XLWorkbook();
        // 1つ以上シートが存在する必要があるのでシートを追加.
        workbook.Worksheets.Add(SheetName);
        // ここでエラー. 
        workbook.SaveAs(FilePath);
        return workbook;
    }

    static IXLWorksheet GetWorksheet(XLWorkbook workbook) {
        if (workbook.TryGetWorksheet(SheetName, out var worksheet)) {
            return worksheet;
        }
        // シートが存在しなければ追加.
        return workbook.Worksheets.Add(SheetName);
    }
}

これを実行すると、 workbook.SaveAs(FilePath); の部分で TypeInitializationException が発生しました。

更に原因を追っていくと、どうやら libgdiplus というライブラリファイルが見つからないことが原因となっているようでした。

homebrewで起きた問題

libgdiplus は GitHub で開発が進められており、 README を基に、依存ファイルを homebrew でインストールし、 make install してみることにしました。

github.com

そして動かない homebrew orz

どうやら OS をアップデートした影響で、下記の問題が発生している様子。

qiita.com

ということで手順に従って権限を変更しようとしたのですが、エラーで失敗しましたorz

仕方がないので、 homebrew を再インストールすることにしました。

再インストール後、自動・手動で求められた依存ファイルのインストールを行い、なんとか homebrew が動作するようになりました。

解決

晴れて libgdiplus がインストールできるぞ! と思ったものの、 git clone した libgdiplus のディレクトリ上で make install を実行してもインストールするものはないよ。とエラーになってしまいました。

結果としては、 homebrew で libgdiplus を検索したときに見つけた、 mono-libgdiplus をインストールすることで解決しました。

いやぁ長かった。

おわりに

ともあれこれで Mac でも C#Excel 操作ができるようになったわけですね。

Enjoy Excel Life!

ってどんな締めや。

ASP.NET CoreでPOST (Unityもちょっとだけ)

はじめに

以前 Controller から受け取れるようにしたので、今回はそれに対する POST リクエストで、色々値を受け渡してみよう、という内容です。

文字列を受け取る

まずは Razor 構文の勉強がてら、文字列を Views/Home/Index.cshtml から渡してみます。

Index.cshtml

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div>Name: < input name="bodyText"/>< /div>
    < input type="submit" />
< /form>
  • asp-controller でルーティングを行う Controller を指定できます。
  • asp-action で Submit ボタンを押したときに呼ばれるメソッドを指定できます。
  • TagHelpersのおかげで入力補完も効いて便利ですね。
  • input タグの name で、呼び出すメソッドの引数を指定します。

で、これを Controller で受け取ります。

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample(string bodyText) {
    // 引数名は Input の name と合わせる必要がある.
    Console.WriteLine(bodyText);
}
~省略~

なお、引数を今回のようにフォームからの入力に限定したい場合は下記のように指定できます。

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample([FromForm] string bodyText) {
    // 引数名は Input の name と合わせる必要がある.
    Console.WriteLine(bodyText);
}
~省略~

他にも、今回は POST ですが、例えば GET でクエリパラメーターを受け取りたい場合、 [FromQuery] を使います。

注意点を付け加えるとすると、メソッドのオーバーロードはできず、例えば引数なしのメソッドを追加すると、 Submit ボタン押下時に AmbiguousActionException が発生します。

配列を受け取る

次は配列を受け取ってみます。

Razor 構文では @{} で C# のコードが書けるため、下記のようなことができます。

Index.cshtml

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div> Name:
        @{
            for (var i = 0; i < 3; i++) {
                
            }
        }
      < /div>
    < input type="submit" />
< /form>

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample(string[] messages) {
    Console.WriteLine(messages[0]);
}
~省略~

キーとなるのは name で、これが同じものを配列(または List )として扱うことができます。

実際のユースケースを踏まえて ID を設定していますが、無くても動作には問題ありません。

HTML の左上の要素から順にセットされます。

自作クラスを受け取る

今度は自作クラスを引数として受け取ってみます。

Message.cs

using System;

namespace WebApplication1.Domains {
    public class Message {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BodyText { get; set; }
        public DateTime LastUpdatedDate { get; set; }
    }
}

※今回は簡略化のため BodyText のみを使いますが、他の要素も同じように渡すことができます。

引数として受け取る

今回は値を受け取る方法を二種類試しました。

一つ目は先ほどと同じように、引数として受け取る方法です。

HomeController.cs

[Route("/PostSample")]
[HttpPost]
public void PostSample([FromForm] Message message) {
    Console.WriteLine(message.BodyText);    
}

Index.cshtml

@page
@using WebApplication1.Domains

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Message message = new Message();
}
< div>@DateTime.Now< /div>
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div>Name: < input name="message.BodyText" />< /div>
    < input type="submit" />
< /form>

余談ですが、上記の message.BodyText のようにして値を渡す方法は、 GET でも使用できます。

このような URL でクエリパラメータを渡すと、クラスとして受け取ることができます。

http://localhost:51817/PostSample?message.Id=1&message.BodyText=sample

HomeController.cs

~省略~
[Route("/PostSample")]
public void PostSample([FromQuery] Message message) {
    Console.WriteLine(message.BodyText);
}
~省略~

BindProperty を使う

引数として渡すのではなく、 BindProperty を使うこともできます。

HomeController.cs

~省略~
[BindProperty]
public Message Message { get; set; }

[Route("/PostSample")]
[HttpPost]
public void PostSample() {
    Console.WriteLine(Message.BodyText);    
}
~省略~

Index.cshtml

@page
@using WebApplication1.Domains

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
< div>@DateTime.Now< /div>
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div>Name: < input name="Message.BodyText" />< /div>
    < input type="submit" />
< /form>

値を他からも使いたい場合などは便利そうです。

asp-for

もう一つ、 Input に name ではなく asp-for を使って指定する方法( ID、Name が自動設定されるとのこと)がありますが、コードビハインドのある Razor View でないと、 viewData が null というエラーが発生したため今回は使用していません。

Unity からリクエストを投げてみる

HTML からだけでなく、 Unity から送ってみるとどうなるでしょうか。

GET リクエス

UnityWebRequest を使ってリクエストを投げることができます。

まずは GET から( ASP.NET Core 側は GET リクエストを受け付けるように変更してください)。

SendSample.cs

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class SendSample : MonoBehaviour {
    public Button SendButton;

    public void Start() {
        SendButton.onClick.AddListener(() => {
            StartCoroutine(Send());
        });
    }
    private IEnumerator Send() {
        UnityWebRequest request = UnityWebRequest.Get(@"http://localhost:51817/PostSample?message.Id=1&message.BodyText=sample");
        yield return request.SendWebRequest();

        if(request.isNetworkError ||request.isHttpError) {
            Debug.Log(request.error);
        }
        else {
            Debug.Log("Form upload complete!");
        }
    }
}

POST リクエス

ASP.NET Core の View のように、クラスをそのまま渡すことはできないため、 JSON 形式にして渡すことにします。

SendSample.cs

using System.Collections;
using Domains;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class SendSample : MonoBehaviour {
    public Button SendButton;

    public void Start() {
        SendButton.onClick.AddListener(() => {
            var message = new Message {
                Id=6,
                BodyText = "sample",
            };
            var json = JsonUtility.ToJson(message);
            
            StartCoroutine(Send(json));
        });
    }
    private IEnumerator Send(string jsonFile) {
        WWWForm formData = new WWWForm();

        // そのまま渡すと{}がエンコードされてしまい、JSONとして認識されなくなるのでbyte[]に変換.
        byte[] postData = System.Text.Encoding.UTF8.GetBytes (jsonFile);
        
        UnityWebRequest request = UnityWebRequest.Post("http://localhost:51817/PostSample", formData);
        request.SetRequestHeader("Accept", "application/json");
        request.SetRequestHeader("Content-Type", "application/json");
        
        request.uploadHandler = new UploadHandlerRaw(postData);
        
        yield return request.SendWebRequest();
        
        if(request.isNetworkError || request.isHttpError) {
            Debug.Log(request.error);
        }
        else {
            Debug.Log("Form upload complete!");
        }
    }
}

ここで重要なのは、 JSON のデータを渡し忘れないことです(白目)

あとコメントにも書きましたが、JSON データを string でそのまま渡すとエンコードされてしまうそうなので、 byte[] で渡します。

JSON を受け取る

先ほど触れた通り、 Unity と ASP.NET Core でクラスを共有することはできないため、 JSON を受け取る必要が出てきます。

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample([FromBody]Message message) {
    Console.WriteLine(message.BodyText);
}
~省略~

ポイントは [FromBody] で、これによって自動でデシリアライズされた値を引数として受け取ることができるようになります。

JSON を扱う場合は必ず [FromBody] をつける必要があり、これを外すと空データになってしまいます。

ただし、 Form データなどは受け取ることができない(不正なメディアタイプと怒られる)ため、注意が必要です。

おわりに

とりあえずデータは渡せるようになりました。

前にも書いた気がしますが、 WebSocket の処理と絡めてみたり、いい加減に Model を触ってみたりもしたいところ。。。

あとは Rx とか。

参照

Unity

【Unity】websocket-sharpのOnMessage

はじめに

前回作った WebSocket であれこれ試そうとしたら、 Unity で websocket-sharp を使って送信されたデータを受け取る、 OnMessage() でエラーが出たので調べた時のメモです。

なお Unity 側のコードはこれと同じです。

http://mslgt.hatenablog.com/entry/2017/05/11/073758

具体的には、OnMessage() が Main(UI) とは別の Thread から実行され、 Unity の処理(例: uGUI の Text に値を入れる、 Transform の値を変更する)を実行しようとするとエラーが発生していました。

WebSocketAccessor.cs

public class WebSocketAccessor : MonoBehaviour {

  public Text MessageText;
  private WebSocket ws;
    
    private void Start () {
    ws = new WebSocket("ws://localhost:5XXXXX/ws");
    
        // メッセージ受信時のイベント.
        ws.OnMessage += (sender, e) => {
          Debug.Log("Received " + e.Data);  


      // ここでエラー.
        MessageText.text = e.Data;

      
        };
        // 接続.
        ws.Connect ();
    }
    ~省略~

で、別 Thread から実行することが問題なのであれば、 Main Thread から実行するよう変更しましょう、というのが今回の内容です。

Main Threadで実行する

Java, Android では runOnUiThread というものがあります。

挙動は名前の通りなのですが、 C# ではこのようなものはなさそうです。

その代わりに、Main Thread で Context を取得しておき、それを通して Main Thread で実行する、ということが可能になります。

WebSocketAccessor.cs

public class WebSocketAccessor : MonoBehaviour {

  public Text MessageText;
  private WebSocket ws;
    
    private void Start () {
    ws = new WebSocket("ws://localhost:5XXXXX/ws");


    // Main ThreadのContextを取得する.
    var context = SynchronizationContext.Current;
    
    
        // メッセージ受信時のイベント.
        ws.OnMessage += (sender, e) => {
          Debug.Log("Received " + e.Data);  


      // Main Threadで実行する.
      context.Post(state => {
        MessageText.text = state.ToString();
      }, e.Data);

      
        };
        // 接続.
        ws.Connect ();
    }
    ~省略~
  • SynchronizationContext.Current を Main Thread で実行すると、 Main Thread の Context を取得することができます。

SynchronizationContext.Post()

SynchronizationContext.Post() から Main Thread での処理を呼び出すことができます。

この第一引数は SendOrPostCallback という delegate method で、第二引数は第一引数に渡す引数です。

この引数の型は Object なので、今回の string 以外のデータも渡すことができますが、 delegate method で型変換は必要です。

SynchronizationContext.Send()

もう一つ似たようなものに SynchronizationContext.Send() というものがあります。

パッと見同じに見えるのですが、どうやら Post() は非同期的に実行されるのに対し、 Send() は同期的に実行される、という違いがあるようです。

そのため、例えば下記の2つを比べると。。。

SynchronizationContext.Post()

// メッセージ受信時のイベント.
ws.OnMessage += (sender, e) => {
            
    Stopwatch watch = new Stopwatch();
    watch.Start();

    context.Post(state => {
    // 1000ミリ秒止める.
        Thread.Sleep(1000);
        MessageText.text = state.ToString();
    }, e.Data);
            
    Debug.Log("finish " + watch.ElapsedMilliseconds);
    watch.Stop();
};

SynchronizationContext.Send()

// メッセージ受信時のイベント.
ws.OnMessage += (sender, e) => {
            
    Stopwatch watch = new Stopwatch();
    watch.Start();

    context.Send(state => {
    // 1000ミリ秒止める.
        Thread.Sleep(1000);
        MessageText.text = state.ToString();
    }, e.Data);
            
    Debug.Log("finish " + watch.ElapsedMilliseconds);
    watch.Stop();
};

Post() は「finish 0」と出力されるのに対し、 Send() は「finish 1009」と出力されました。

なお、上記で Debug.Log("finish " + watch.ElapsedMilliseconds); が実行され、 Editor 上で表示されるタイミングは(おそらく MainThread で実行されるとかその辺の理由で)どちらも同じのため、注意が必要かもしません。

おわりに

以前 SpringBoot で動かしていた時には OnMessage も MainThread で受け取れていた気がしたのですが、今回変わった理由はまだよくわかっていません。

まぁ websocket-sharp 側で変更があったのかなぁ、というところではありますが。

次回も WebSocket(ASP.NET Core 側)で遊んでみる予定です。

参照

ASP.NET CoreでWebSocket

はじめに

以前 Spring boot で WebSocket を使う、てなことをやりましたが、今回は ASP.NET Core を使ってやってみます。

なぜ急に WebSocket か? お察しください

なおリアルタイムで処理を行う機能としては ASP.NET SignalR というものがあって、 ASP.NET Core では ver.2.1 から使えるとのことなので、こちらも近いタイミングで試してみたいと思います。

なおクライアント側は下記と同じく websocket-sharp を使うことにします。

http://mslgt.hatenablog.com/entry/2017/05/11/073758

インストール

ASP.NET Core では Microsoft による WebSocket パッケージがありますのでそれを使用します。

NuGet で Microsoft.AspNetCore.WebSockets パッケージをインストールすれば OK です。

簡単ですね。

サンプルコードを試してみる

まずはサンプルコードを試してみることにします。

docs.microsoft.com

github.com

↑を試してみると、確かに WebSocket で接続することができました。

ただ、(サンプルコードとしては素晴らしいのですが) Startup クラスが少々長くなってしまうのが気になりました。

Configure() の中身を見てみると、関連するコードは(オプションを除くと)以下のようでした。

Startup.cs

~省略~
  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
    ~省略~
    app.UseWebSockets();
    ~省略~
    app.Use(async (context, next) => {
        if (context.Request.Path == "/ws") {
            if (context.WebSockets.IsWebSocketRequest) {
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
                await Echo(context, webSocket);
            }
            else {
                context.Response.StatusCode = 400;
            }
        }
        else {
            await next();
        }
    });
  }
~省略~

WebSocket を有効にする UseWebSockets() はともかく、接続するところは Controller クラスでもできるんじゃね?

と思ってコピペしたらエラーになりました。

ですよね~/(^o^)\

HomeController.cs

~省略~
  [Route("ws")]
  public async void RouteWebsocketAccess() {
      if (this.HttpContext.WebSockets.IsWebSocketRequest) {
          WebSocket webSocket = await this.HttpContext.WebSockets.AcceptWebSocketAsync();
          await Echo(this.HttpContext,  await this.HttpContext.WebSockets.AcceptWebSocketAsync());
      }
      else {
          this.HttpContext.Response.StatusCode = 400;
      }
  }
~省略~

WebSocket のリクエストかどうかを確認する IsWebSocketRequest は正しく動作していましたが、実際にデータの送受信をする Echo の部分でエラーが発生していました。

The remote party closed the WebSocket connection without completing the close handshake.

また、 WebSocket のリクエストではなかった場合の、 this.HttpContext.Response.StatusCode も正しく動作していませんでした。
(Startup.cs で実行した場合は前回の 404 と同じように処理されます)

Controller クラスではできないのか、見様見真似で書いたものの間違いがあってできないのかはわからないのですが、とりあえずこのままではダメ、ということがわかりました。

とりあえず今回は別の方法を選ぶことにします。

app.Use()とapp.Map()

tamafuyou.hatenablog.com

この記事を見てみると、 Startup では app.Map() を使ってマッピングを行い、先ほどの app.Use() の部分をそのまま chatServer.Map に移していました。

 app.Map("/ws", chatServer.Map);

では、この app.Map() や app.Use() って何なのでしょうか。

というのを少しだけ見てみます。

docs.microsoft.com

まだわかるようなわからないようなというところですが、

そもそもまずこの Configure というメソッドは、

  1. HTTP アクセスがあった場合に毎回呼び出される
  2. DB 、ログ出力、ルーティングなどを行うミドルウェアを利用している場合に、それらの処理を呼び出す

というものであり、 app.Use() は 2. を実行するためのもの、ということのようです。

ルーティングのようにデフォルトで用意されているものは、 app.UseMvc() のようなメソッドが用意されているのでそれを使う、と。

で、 app.Map() というのは特定のパスに対して処理を割り当てるもの、ということのようです。

このミドルウェアは複数扱うことができ、 app.Use() などの中から app.Next() を呼ぶと次のミドルウェアの処理に移すことができます。

先ほどの WebSocket の処理で、 URL が ~/ws ではなかった場合に app.Next() を呼び忘れると該当のページが表示されないため注意が必要です。

今回登場しませんでしたが、 app.Run() を実行すると処理がそこで終わってしまうため、最後に実行する必要があります。

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
  if (env.IsDevelopment()) {
      app.UseBrowserLink();
      app.UseDeveloperExceptionPage();
  }
  app.Run(async (context) => {
    await Task.Run(() => {
          Console.WriteLine("hello goodbye");
    });
  });
  // ここから先の処理が実行されない/(^o^)\.
  app.UseStaticFiles();

  ~省略~

}

WebSocketを使うコードを見る

ようやく今回のテーマである(寄り道しすぎ)、 WebSocket を利用するコードを見てみることにします。

先ほどから登場しまくりである下記のサンプルコードを参考に(ほぼコピペ)、 WebSocket で接続し、メッセージを受信したら接続先に返す、という内容となっています。

ASP.NET Core での Websocket のサポート | Microsoft Docs

ASP.NET CoreでWebSocketを使用するサンプルを作ってみたよ( `ー´)ノ - 眠いしお腹すいたし(´・ω・`)

WebSocketController.cs

using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication1.Controllers {
    public class WebsocketController {
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    // HTTPリクエストの内容が WebSocket のものであればAcceptし、メッセージの受信・送信を行うメソッドに渡す.
                    WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
                    await Echo(context, webSocket);
                }
                else {
                    // HTTPリクエストの内容が WebSocket のものでなければエラー.
                    context.Response.StatusCode = 400;
                }
            });
            // app.Map("/ws", websocket.Map); でマッピングしてるので他のパスに対する処理はしない.
        }

        // 引数のHttpContextは今回未使用. やりたいことがこれだけなら不要かも.
        private async Task Echo(HttpContext context, WebSocket webSocket) {

            // 送受信するデータ(buffer)のサイズ指定.
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);

            // 接続がCloseされるまでメッセージの送受信を繰り返し.
            while (result.CloseStatus.HasValue == false) {
                await webSocket.SendAsync(new ArraySegment< byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

                result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            }
            // Whileを抜けたらClose処理.
            // 接続が Close されると、result.CloseStatusに「NormalClosure」のような値が入ってくる.
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
        }
    }
}

接続は Map() の context.WebSockets.AcceptWebSocketAsync() で行われて、メッセージの送受信は Echo() で行われるようですね。

メッセージの送受信

先ほどのコードで触れていなかったメッセージの送受信について追いかけてみます。

メッセージの受信

WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);

送信の引数にもなっていますが、 ArraySegment は配列の一部を持つもので、渡している buffer( byte の配列)にはクライアントから送信されたデータが格納されます。

第二引数には処理をキャンセルする必要がある場合にその通知を渡すもの( CancellationToken )ということなのですが、空で渡しています。

送受信をキャンセルするようなこともできる、ということなのでしょうか。

戻り値となる WebSocketReceiveResult は、受信したデータそのものは持っておらず、受信データの型( MessageType )や接続が Close されたか( CloseStatus )などの情報を持っています。

試しに下記のようにログを出してみました。

WebSocketController.cs

 byte[] buffer = new byte[1024 * 4];

Console.WriteLine("before bLength " + buffer.Length + " b0 " + buffer[0]);

WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);

Console.WriteLine("bLength " + buffer.Length + " b0 " + buffer[0] + " rCount " + result.Count + " rEnd " + result.EndOfMessage + " rType " + result.MessageType);

while (result.CloseStatus.HasValue == false) {
    await webSocket.SendAsync(new ArraySegment< byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

    Console.WriteLine("while bLength " + buffer.Length + " b0 " + buffer[0] + " rCount " + result.Count + " rEnd " + result.EndOfMessage + " rType " + result.MessageType);

    result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
}

で、クライアントから「世界さん、チーッす!」という文字列を1回送信したところ、結果は下記のようになりました。

before bLength 4096 b0 0
bLength 4096 b0 228 rCount 30 rEnd True rType Text
while bLength 4096 b0 228 rCount 30 rEnd True rType Text

result.Count

result.Count で取得できる 30 という数値。

これは、 buffer にあらかじめ設定している 4096 という要素数のうち、実際にデータが格納されている(つまり値が 0 でない)要素数を示しているようです。

メッセージの送信

await webSocket.SendAsync(new ArraySegment< byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

送信側も引数は受信とあまり変わらない(今回は受け取ったデータをそのまま渡しているため)ようです。

buffer

0 から buffer の中のデータが格納された要素数分を保持する ArraySegment を渡しています。

result.EndOfMessage

( buffer の要素数などの静減から)メッセージを複数に分割して送信したい場合は false を渡してやれば良さそうです。

サンプルでは受信したデータをそのまま送信していますが、例えば固定で文字列を追加して送信したい場合、下記のようにすれば実現できます。

// 追加する文字列(byteの配列として持つ).
byte[] additionalBytes = System.Text.Encoding.UTF8.GetBytes("hello: ");
// 追加する文字列を受信したデータとマージする.
byte[] newBytes = new byte[additionalBytes.Length + result.Count];
additionalBytes.CopyTo(newBytes, 0);
ArraySegment< byte> receivedDataSegment = new ArraySegment< byte>(buffer, 0, result.Count);
receivedDataSegment.CopyTo(newBytes, additionalBytes.Length);

// データを送信する.
await webSocket.SendAsync(newBytes, result.MessageType, result.EndOfMessage, CancellationToken.None);    

配列のマージについてはもう少しうまいやり方があるかもしれませんが。

おわりに

今回はひたすらサンプルを追っかける回になってしまいました。

今回に限らずですが、サンプルコードがちゃんと動くってのはありがたいですねぇ(..)_

もうちょっとだけ続くんじゃ、ということで、次回はもう少しこれをベースに遊んでみたいと思います。

参照

ArraySegment

ASP.NET Coreに触れてみる 2

はじめに

次はモデルの話に行く予定でしたが、読んでる本MVC のコントローラーのルーティングの話に行ったのでその話を。

依存の追加について

それはそれとして、前回 Dependency Injection の話で、サービスに Inject する方法を以下のように書きました。

~省略~
public class Startup {
    public void ConfigureServices(IServiceCollection services) {

        services.AddSingleton< IDiSample, DiSample>();

        services.AddMvc();
    }
    ~省略~
  }
~省略~

これは下記のようにも書くことができるようです。

~省略~
public class Startup {
    public void ConfigureServices(IServiceCollection services) {

        これも OK
        services.AddSingleton< IDiSample>(new DiSample());

        services.AddMvc();
    }
    ~省略~
  }
~省略~

ルーティングについて

ASP.NET Core でのルーティングには、規約ベースと属性ベースが存在します。

規約ベースのルーティング

規約ベースのルーティングは、 Startup の Configure で MVC を有効にする( UseMvc() を実行する)時にコントローラーなどへのマッピングを名前指定で行います。

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace WebApplication1 {
    public class Startup {
        ~省略~
        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            ~省略~
            
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    // controllerとして指定している値+ Controllerという名前のクラスの,
                    // actionで指定している名前のメソッドが実行される.
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
            
        }
    }
}

これで localhost:5XXX や localhost:5XXX/Home 、 localhost:5XXX/Home/Index にアクセスしたとき Controllers フォルダに作成した HomeController.cs の Index() が実行されるようになります。

HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        public IActionResult Index() {
            // Views/Home に作成した Index.cstml を表示する.
            return View();
        }        
    }
}

なお上記 Startup でのマッピングは、代わりに app.UseMvcWithDefaultRoute() を実行しても同じ動作となります。 (controller や action 名を変更する場合は明示的に指定する必要があります)

前回登場した Razor View では、(自作の)コントローラーを使用していなかったので app.UseMvc() (引数なし)を実行していました。 なおこれは app.UseMvc(routes => { }); と同じ動作をします。

また前回の Razor View は Pages フォルダにあったのに対し、今回使用するビューは Views/Home にあり、そのまま移動させてしまうとエラーとなるため、再度作り直したほうが良さそうです。

上記ではコントローラー名、アクション名を URL と合致させていますが、別々のものを指定することもできます。

Startup.cs

app.UseMvc(routes => {
        routes.MapRoute(
            name: "default",
            template: "{id?}",
            defaults: new { controller = "Home", action = "Index" });
    });

これで localhost:5XXX または localhost:5XXX/0 といった URL でのみ指定のページが表示されるようになります。

属性ベースのルーティング

Spring boot だと @Controller などのアノテーションをつけて指定する方法がこちらです。 (見た感じ、というだけで実際に同じ動作かと言われると自信がありませんが)

Startup.cs

~省略~
namespace WebApplication1 {
    public class Startup {
        ~省略~
        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            ~省略~
            
            app.UseMvc();
            
        }
    }
}

HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        [Route("")]
        [Route("Home")]
        [Route("Home/Index")]
        public IActionResult Index() {
            return View();
        }        
    }
}

これで localhost:5XXX や localhost:5XXX/Home 、 localhost:5XXX/Home/Index にアクセスしたとき Index() が実行されます。

コントローラーのクラス名を HomeController としていますが、マッピングする URL と合致はしていなくても OK です。

ただし、ビューのフォルダ名とは合致していないとエラーになるので、直接指定する必要があります。

HomeController.cs

public IActionResult Index() {
    return View("~/Views/Home/Index.cshtml");
}

もう少し使ってみないといけないとは思いますが、個人的には属性ベースの方が好みです。
マッピングする URL と呼ばれるメソッドやコントローラーがそばにあるあたりが。

キャッチオールルートパラメーター

例えば localhost:5XXX/Home/ 以下のすべての URL にアクセスされた場合に一括で処理をしたい場合。

HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        [Route("")]
        [Route("Home")]
        [Route("Home/{*article}")]
        public IActionResult Index() {
            return View();
        }        
    }
}

{*article} を指定することで、例えば例えば localhost:5XXX/Home/Index であろうと localhost:5XXX/Home/Sample であろうとすべて Index() が呼ばれるようになります。

これをキャッチオールルートパラメーターというそうです。
安全保障貿易管理に出てくるキャッチオール規制を思い出す名前ですが。

HTTPリクエス

GET、POST などの HTTP リクエストの指定をすることもできます。

HomeController.cs

public class HomeController : Controller {
    [HttpGet]
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    public IActionResult Index() {
        return View();
    }        
}

これで該当の URL に GET リクエストが渡された場合のみ Index() が実行されるようになります。

※規約ベースのルーティングで、上記を実現する方法は見つけられませんでした。。
IRouteBuilder の MapGet などで HTTP リクエストを指定する方法はあるのですが、ビューをどう表示するかが見つけられずorz

また Web API を作りたい場合など、 URL マッピングをクラス全体に対してつけることも可能です。

以下は Visual Studio でコントローラークラスをジェネレートするとできるものです(名前は任意)。

MessageController.cs

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers {
    [Produces("application/json")]
    [Route("api/Message")]
    public class MessageController : Controller {
        // GET: api/Message
        [HttpGet]
        public IEnumerable Get() {
            return new string[] { "value1", "value2" };
        }

        // GET: api/Message/5
        [HttpGet("{id}", Name = "Get")] 
        public string Get(int id) {
            return "value";
        }
        
        // POST: api/Message
        [HttpPost]
        public void Post([FromBody]string value) {
        }
        
        // PUT: api/Message/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value) {
        }
        
        // DELETE: api/ApiWithActions/5
        [HttpDelete("{id}")]
        public void Delete(int id) {
        }
    }
}

これで localhost:5XXX/api/Message に例えば Web ブラウザでアクセスすれば、"value1"、"value2" という値が JSON で表示されます。

エラーページ

ここまでで URL のマッピングや表示するビューについては少しわかった気がするのですが、このままだと 404 などのエラーページが表示されません。

これを解決するには、 Startup.cs の Configure で設定が必要です。

Startup.cs

~省略~
namespace WebApplication1 {
    public class Startup {
        ~省略~
        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            ~省略~
            
            app.UseStatusCodePages();
            
        }
    }
}

これで例えば存在しない URL にアクセスすると、 Status Code: 404; Not Found のようなテキストが表示されるようになります。

ここで例えばビューとして用意したページを見せたい場合、下記のように指定することができます。

Startup.cs

~省略~
namespace WebApplication1 {
    public class Startup {
        ~省略~
        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            ~省略~
            
            app.UseStatusCodePagesWithRedirects("/Error/{0}");
            
        }
    }
}

この {0} には例えば 404 エラーなら「404」という値が入るので、

HomeController.cs

~省略~
[Route("/Error/404")]
public IActionResult Error404() {
    return View("~/Views/Home/ErrorPageNotFound.cshtml");
}
~省略~

あとは(今回の場合) Views/Home に ErrorPageNotFound.cshtml を作れば完了です。

ErrorPageNotFound.cshtml

@{
    ViewData["Title"] = "ErrorPageNotFound";
}

< h2>Page Not Found< /h2>
< div>おめーに見せるページはねぇ!< /div>

おわりに

今回解決できなかった(自分の中でまとまらなかった)点として、コントローラーの分割があります。

途中で触れた通りコントローラーで実行されるメソッドを URL で指定するわけですが、例えばビューを表示する処理のみをコントローラー1で行い、CRUD 処理をコントローラー2で行う、といった分割をするべきかどうか。

まぁ作るものの規模にもよるでしょうし、もう少し実際に作ってみながらその辺りは確かめてみたいと思います。

次回こそモデルの話。。。のハズ。

参照

ASP.NET Coreに触れてみる 1

はじめに

前から気にはなっていた、 ASP.NET Core を触ってみることにしました。

まずはチュートリアルから。。。と思ったのですが、Razor View にしろ MVC Web アプリにしろ、プロジェクトを Web Application のテンプレートで作った時点で複数ページ分ガッツリ作られてしまいます。

docs.microsoft.com

で、チュートリアルの内容としてもこれをベースに Model を追加したりすることになるため、おねがい、チョ待って!チョ待って!となりました(これはこれで進めたいのですが)。

折よく手に入れた本がプロジェクトを Empty で作って一つずつ見ていく、という内容だったこともあり、もうちょっと基本的なところから見てみたいと思い、その内容を気力が続く限り書き残しておきたいと思いました。

Programming ASP.NET Core

生成されたフォルダ・ファイル

まずはプロジェクトを Empty で作って、生成されるフォルダとファイルを見てみます。

  • wwwroot: フォルダ。CSS や画像などの静的ファイルを置く(有効にした場合)。 Web アプリのルートディレクトリとなる。
  • Program.cs: メインクラス。使用するサーバーや Startup として使用するクラスの指定などを行う。
  • Startup.cs: Startup クラス。DI の Inject 対象クラスの登録や Razor View の有効化などを行う。

プロジェクトを Web Application で生成すると、これに加えて Pages というフォルダが作られ、そこに Razor View のファイルが置かれます。

Razor View

先ほどから登場している Razor View 。

これは、 jspasp のように、 HTML にサーバーサイドの言語(ここでは C#VB.NET )で処理が書ける、といったもののようです。

Xamarin や WPF でいう XAML とコードビハインドのように、Index.cshtml <-> Index.cshtml.cs というセットで処理を書きます。

docs.microsoft.com

後述しますが、 Startup クラスで MVC が有効になっている場合、アクセスした URL のパスに合わせて Razor View が表示されます。

例えば Pages/About.cshtml がある場合、 localhost:5XXX/About にアクセスすると About.cshtml が表示されます。

またコードビハインドには OnGet メソッドがデフォルトで作成されており、ページアクセス時に呼ばれます。

ここから、(少なくともデフォルトでは) Routing の役割を担う Controller クラスは自作せず、自動で Routing されるらしいことがわかります。

また各ページごとに GET や POST の処理を行う、と。

特殊な Razor View

基本的な動作は上記の通りですが、いくつか特別な動きをするものがあります。

Index.cshtml

Index と名付けられたページは、トップドメインで表示されます( localhost:5XXX )。

ViewStart.cshtml と Layout.cshtml

head・body タグをはじめ、各ページ共通で表示したい要素がある場合 Layout にまとめることができます。

この Layout 、デフォルトでは _Layout.cshtml となっていますが、変更することができます。

どのファイルを Layout とするかは _ViewStart.cshtml で指定されており、これを変更することで別名のファイルが指定できます。

_ViewStart.cshtml

@{
    Layout = "_Layout";
}

ただし上記の場合では _Layout.cshtml というファイルがないと実行時に例外が発生します。

なおコンパイルエラーにはならないものの、 Visual Studio (または ReSharper )が赤く表示してくれるので事前の確認も可能ではあります。

また Layout ファイルでは、各ページで定義している表示内容をどこに表示するかを、 @RenderBody() で指定します。

_Layout.cshtml

< !DOCTYPE html>
< html>
< head>
    < meta charset="utf-8" />
< /head>
< body>
    < h1>世界さん、ちーっす< /h1>
    @RenderBody()
< /body>
< /html>

Layout ファイルに @RenderBody() が含まれていないと、これまた実行時に例外が発生します。

こちらは赤く表示もされないため、より注意が必要です。

Program.cs

前述の通り、メインクラスです。

デフォルトでは WebHost.CreateDefaultBuilder を実行し、埋め込みのサーバー( Kestrel )の使用やルートディレクトリの指定、 Startup クラスの指定などを行い、アプリを実行します。

Startup.cs

Startup クラスではデフォルトで2つのクラスが生成されます。

  • ConfigureServices
  • Configure

ConfigureServices に対して DI コンテナに追加したいクラスや MVC の登録を行い、Configure で有効化する、という動きをするようです。

また Configure では Error の発生時に表示するページを指定することもできます。

これにより開発時には詳細なエラー内容がわかる開発者向けのページを表示したり、本番環境では不要な情報を見せない、といったことが可能になります。

Startup.cs

public void ConfigureServices(IServiceCollection services) {
    // DI コンテナへのクラス( interface )の追加(サービスに登録).
    services.AddSingleton();
    // MVC を追加する( Razor View を使うため).
    services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
    if (env.IsDevelopment()) {
        // 開発環境においては開発者向けのエラーページを表示.
        app.UseDeveloperExceptionPage();
    }
    // wwwroot に置かれている静的ファイルを有効にする.
    app.UseStaticFiles();
    // MVC を有効にし、URL アクセス時に Razor View が表示されるようにする.
    app.UseMvc();
}
  • ConfigureServices で IServiceCollection に対して AddMvc() を実行せずに Configure で UseMvc() を実行すると InvalidOperationException が発生します。
  • Configure で UseMvc() を実行しないと、真っ白のページが表示されます( Razor View 以外に表示するものを用意していない場合)。

その他 Startup でできる(やるべき)ことはまだまだたくさんあるようですが、おいおい調べていきたいと思います。

docs.microsoft.com

Dependency Injection

ASP.NET Core では Dependency Injection を、

  1. Startup.cs の ConfigureServices で対象クラス( interface )をコンテナに追加(サービスに登録)
  2. 1.のクラスを利用するクラスのコンストラクタで受け取る

という流れで実現しています。

今回は任意で interface とその実装クラスを作り、それを Index.cshtml.cs で受け取ってみます。

IDiSample.cs

  • DI で Inject する interface
namespace WebApplication1
{
    public interface IDiSample {
        void Say();
    }
}

DiSample

  • IDiSample.cs の実装クラス
using System;

namespace WebApplication1 {
    public class DiSample: IDiSample {
        public void Say() {
            Console.WriteLine("Hello DI");
        }
    }
}

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace WebApplication1 {
  public class Startup {
    public void ConfigureServices(IServiceCollection services) {

        // サービスに Inject する interface, 実装クラスを(ここではシングルトンとして)追加.
        services.AddSingleton< IDiSample, DiSample>();

        services.AddMvc();
    }
    ~省略~
  }
}

Index.cshtml.cs

using System;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication1.Pages
{
    public class IndexModel : PageModel {

        // コンストラクタで登録されたクラスを受け取り.
        public IndexModel(IDiSample s) {
            s.Say();
        }
        
        public void OnGet() {
            Console.WriteLine("hello");
        } 
    }
}

おわりに

プロジェクト生成時に Web Application を選ぶと、数ページ分ガッツリコードが出てくるのには面喰いましたが、一つ一つたどっていくとちょっとは理解できたような気がします。

次回は Model を Scaffold で生成する辺りの話。。。のはず。

参照

【C#】ClosedXMLでExcelのデータ読み込み

はじめに

みなさん Excel 使ってますか Excel

今日も(多分)外道な使い方をしている増井です。どうもこんにちは。

今回は ClosedXML を使って、 Excel(Spreadsheet) のデータを読み込んでみることにします。

https://github.com/ClosedXML/ClosedXML

基本的な使い方は Wiki に書かれていますが、いくつかやりたかったことが見つけられなかったため、備忘録的に書き残しておきます。

https://github.com/ClosedXML/ClosedXML/wiki

コード

今回は WPF を使って、ボタンを押したら指定のファイルを読み込む、ということにします。

MainWindow.xaml

< Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    < Grid>
        < Button Name="LoadFile1" Content="File1" HorizontalAlignment="Left" Margin="155,86,0,0" VerticalAlignment="Top" Width="75" Click="LoadFile1_Click"/>
    < /Grid>
< /Window>

デフォルトでできる XAML にボタンを追加しただけです。

MainWindow.xaml.cs

using System.Windows;
using WpfApp1.FileAccessor;

namespace WpfApp1 {
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }
        private void LoadFile1_Click(object sender, RoutedEventArgs e) {
            FirstExcelAccessor accessor = new FirstExcelAccessor();
            accessor.Load();
        }
    }
}

ボタンイベントを追加しました。

FirstExcelAccessor.cs

using System;
using System.Linq;
using ClosedXML.Excel;

namespace WpfApp1.FileAccessor {
    class FirstExcelAccessor {
        public void Load() {
            string path = @"ファイルのパス";
            // Excel(Book を開く)
            using (XLWorkbook workbook = new XLWorkbook(path)) {
              // シート、セルにアクセスして必要な処理を行う.
            }
        }
    }
}

今回はファイルのパスを固定で持たせています。
必要に応じて Explorer から取得するなどしてください。

特定のセルの列・行番号を取得する

workbook.Worksheet("シート名") でシートを、 worksheet.Cell("セル名") で取得できます。

また、取得したシートの列数、行数を取得するには下記のようにします。

using (XLWorkbook workbook = new XLWorkbook(path)) {
    using (IXLWorksheet worksheet = workbook.Worksheet("Sheet1")) {
        IXLCell cell = worksheet.Cell("calc");
        int rowNum = cell.Address.RowNumber;
        int columnNum = cell.Address.ColumnNumber;

        // セルの値を string として取得.
        var value = cell.GetString();
    }
}

セルに計算式が入っている場合、 cell.GetString() で取得できるのは計算後の値です。

セルの名前からシートを探す

セルの名前だけがわかっていて、どのシートにそのセルがあるかはわからない場合。

workbook.Cell や workbook.Range でいけそうな気がしたのですが、うまく見つけられず null になってしまうので下記のようにしました。

https://github.com/ClosedXML/ClosedXML/wiki/Accessing-Named-Ranges

var foundSheet = workbook.Worksheets
    .FirstOrDefault(sheet => sheet.Cell("world") != null);

if (foundSheet != null) {
    Console.WriteLine(foundSheet.Name);
}

コードとしてはシンプル(な気がする)ですが、全シートを見ることになるので特にシート数が多かったり、頻繁に検索する必要がある場合などは他の方法を考えたほうが良いかもしれません。