ASP.NET Core webapiでcsvを返す

経緯

DBとかにためてたデータをクエリしてcsvとしてダウンロードできるようなweb apiを作りたかった。

手順

.NET Coreのインストー

現在(2017/07/19)の最新版は.NET Core 2.0 Preview 2なのでここからダウンロードする。

インストーラを起動してぽちぽちすればインストール完了。

インストール完了したらPowerShelldotnet --versionしましょう。

PS C:\Users\ghken> dotnet --version
2.0.0-preview2-006497

ちゃんと2.0のPreview 2が入ってるのが確認できます。

プロジェクトの生成

dotnet coreでは作成するアプリケーションの種類ごとにテンプレートが用意されているので確認してみましょう。

PS C:\Users\ghken> dotnet new
Template Instantiation Commands for .NET Core CLI

Usage: new [options]

Options:
  -h, --help          Displays help for this command.
  -l, --list          Lists templates containing the specified name. If no name is specified, lists all templates.
  -n, --name          The name for the output being created. If no name is specified, the name of the current directory
is used.
  -o, --output        Location to place the generated output.
  -i, --install       Installs a source or a template pack.
  -u, --uninstall     Uninstalls a source or a template pack.
  --type              Filters templates based on available types. Predefined values are "project", "item" or "other".
  --force             Forces content to be generated even if it would change existing files.
  -lang, --language   Specifies the language of the template to create.


Templates                                         Short Name       Language          Tags
--------------------------------------------------------------------------------------------------------
Console Application                               console          [C#], F#, VB      Common/Console
Class library                                     classlib         [C#], F#, VB      Common/Library
Unit Test Project                                 mstest           [C#], F#, VB      Test/MSTest
xUnit Test Project                                xunit            [C#], F#, VB      Test/xUnit
ASP.NET Core Empty                                web              [C#]              Web/Empty
ASP.NET Core Web App (Model-View-Controller)      mvc              [C#], F#          Web/MVC
ASP.NET Core Web App (Razor Pages)                razor            [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
ASP.NET Core Web API                              webapi           [C#]              Web/WebAPI
Nuget Config                                      nugetconfig                        Config
Web Config                                        webconfig                          Config
Solution File                                     sln                                Solution
Razor Page                                        page                               Web/ASP.NET
MVC ViewImports                                   viewimports                        Web/ASP.NET
MVC ViewStart                                     viewstart                          Web/ASP.NET


Examples:
    dotnet new mvc --auth Individual
    dotnet new razor
    dotnet new --help

今回はwebapiを使ってみます。

webapiのプロジェクトを生成するにはdotnet new webapiを実行すればいいのですが、compoer create-projectrails newと違ってプロジェクトのフォルダは生成されないのであらかじめプロジェクトのフォルダに移動したうえで実行しましょう。

PS C:\Users\ghken> mkdir csvapi


    ディレクトリ: C:\Users\ghken


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2017/07/19     17:27                csvapi

PS C:\Users\ghken> cd csvapi
PS C:\Users\ghken\csvapi> dotnet new webapi
The template "ASP.NET Core Web API" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/template-3pn for details.

Processing post-creation actions...
Running 'dotnet restore' on C:\Users\ghken\csvapi\csvapi.csproj...
  Restoring packages for C:\Users\ghken\csvapi\csvapi.csproj...
  Restore completed in 26.11 ms for C:\Users\ghken\csvapi\csvapi.csproj.
  Generating MSBuild file C:\Users\ghken\csvapi\obj\csvapi.csproj.nuget.g.props.
  Generating MSBuild file C:\Users\ghken\csvapi\obj\csvapi.csproj.nuget.g.targets.
  Restore completed in 1.75 sec for C:\Users\ghken\csvapi\csvapi.csproj.


Restore succeeded.

これでプロジェクトが生成されました。

生成されたものを確認しておきましょう。

PS C:\Users\ghken\csvapi> ls


    ディレクトリ: C:\Users\ghken\csvapi


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2017/07/19     17:27                Controllers
d-----       2017/07/19     17:27                obj
d-----       2017/07/19     17:27                wwwroot
-a----       2017/07/19     17:27            178 appsettings.Development.json
-a----       2017/07/19     17:27            228 appsettings.json
-a----       2017/07/19     17:27            582 csvapi.csproj
-a----       2017/07/19     17:27            623 Program.cs
-a----       2017/07/19     17:27            874 Startup.cs

動かしてみる

dotnet coreのアプリケーションを実行するにはdotnet runコマンドを使います。

PS C:\Users\ghken\csvapi> dotnet run
Hosting environment: Production
Content root path: C:\Users\ghken\csvapi
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

どうやらローカルの5000番ポートで何やら動き始めたようです。

linuxとかならcurl使うんですがwindowsに入れてないのでInvoke-WebRequestを使って動作確認してみましょう。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000
Invoke-WebRequest : リモート サーバーがエラーを返しました: (404) 見つかりません
発生場所 行:1 文字:1
+ Invoke-WebRequest http://localhost:5000
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

ルートにアクセスすると404が返ってきます。

何をしてるのか把握するためにコードを読んでみましょう。

Controllersってフォルダがあるのでその中を見てみるとValuesController.csというファイルがあります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace csvapi.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

[Route("api/[controller]")]って書いてあるので/api/valuesにアクセスしたら何か起こりそうな気がします。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api/values


StatusCode        : 200
StatusDescription : OK
Content           : ["value1","value2"]
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Content-Type: application/json; charset=utf-8
                    Date: Wed, 19 Jul 2017 08:43:11 GMT
                    Server: Kestrel

                    ["value1","value2"]
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Content-Type, application/json; charset=utf-8], [Date, Wed, 19 Jul 2017 08:43:11 GMT], [Server, Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 19

contentとして["value1", "value2"]が返ってきているのでどうやらGet()が実行されたようです。

CSVを返す

Controllerのメソッドごとに返値の型がバラバラなのでこれをCSVっぽい型にすればいいんだろうか。

Controllerのメソッドの返値はIActionResultのオブジェクトで、ドキュメント.aspx)を読んだところ、任意の型を返しておけばActionResultでラップしてくれるらしい。

配列とかを返せばそのままjsonでレスポンスしてくれる。

ではCSVはどうするか。

ContentResultってのがあるのでそれを使ってValuesControllerのGetを書き換えてみる。

ヘルパー関数としてContentが用意されていて、第一引数にコンテンツの文字列、第二引数にContent-Typeを入れてやればいいらしい。

public ContentResult Get()
{
    return Content("hoge, fuga", "text/csv");
}

アクセスしてみる。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api/values


StatusCode        : 200
StatusDescription : OK
Content           : hoge, fuga
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 10
                    Content-Type: text/csv
                    Date: Wed, 19 Jul 2017 08:56:33 GMT
                    Server: Kestrel

                    hoge, fuga
Forms             : {}
Headers           : {[Content-Length, 10], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 08:56:33 GMT], [Server, Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 10

無事text/csvで取得できました。

パスを変えてみる

api/valuesのままでは使いづらいのでControllerを追加して別のpathでリクエストを受けるようにしてみましょう。

今回は/apiでリクエストを受けるようなControllerを追加したいと思います。

Controller/RootController.csを作成してそれっぽく実装してみます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace csvapi.Controllers
{
    [Route("/api")]
    public class RootController : Controller
    {
        [HttpGet]
        public ContentResult Get() => Content("hoge, fuga", "text/csv");
    }
}

これで行けるか。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api


StatusCode        : 200
StatusDescription : OK
Content           : hoge, fuga
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 10
                    Content-Type: text/csv
                    Date: Wed, 19 Jul 2017 09:10:31 GMT
                    Server: Kestrel

                    hoge, fuga
Forms             : {}
Headers           : {[Content-Length, 10], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 09:10:31 GMT], [Server, Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 10

成功です。

クエリストリングを扱う

条件をを受けとってそれによって加工したデータを返したいので、クエリストリングを扱う方法を調べます。

ControllerクラスのRequestにQueryプロパティが存在しているのでそれが使えそう。

keyにhogeがあればそれを、なければhogeを使って出力するようにしてみた。

public ContentResult Get()
{
    var hoge = Request.Query.ContainsKey("hoge") ? Request.Query["hoge"].First() : "hoge";
    return Content(hoge + ",fuga", "text/csv");
}

実行。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api?hoge=fuga


StatusCode        : 200
StatusDescription : OK
Content           : fuga,fuga
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 9
                    Content-Type: text/csv
                    Date: Wed, 19 Jul 2017 09:24:35 GMT
                    Server: Kestrel

                    fuga,fuga
Forms             : {}
Headers           : {[Content-Length, 9], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 09:24:35 GMT], [Server, Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 9



PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api


StatusCode        : 200
StatusDescription : OK
Content           : hoge,fuga
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 9
                    Content-Type: text/csv
                    Date: Wed, 19 Jul 2017 09:24:40 GMT
                    Server: Kestrel

                    hoge,fuga
Forms             : {}
Headers           : {[Content-Length, 9], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 09:24:40 GMT], [Server, Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 9

うまくいきました!

ドコモショップでXperia XZ Premiumの端末だけを入手した話

結論から書くと、新規に回線を契約したうえで端末を一括購入し、当月中に解約するといいよって話です。

去年から使ってたXperia X Performanceの調子がここ最近すごく悪くて(タップしてから反応するまでに数秒かかるとか発熱がやばいとか)我慢の限界だったのでスマホを新調しました。

新しく買う候補に求めるものとしては、

  • 今の端末より性能がいい
  • 即日で手に入る

だったので、候補としては、

のどれかかなぁ、となったんですがXperia好きなので結局Xperia XZ Premiumを買うことにしました。

で、現状IIJのsimを使っていたので、どうやってこのsimが使える端末を入手するのか(公式が端末単体では売ってくれてない)という話なんですが、今回は素直にdocomoショップで買うことにしました。

といっても、docomoショップでは端末単体で購入することができないので、一番安い回線に加入して本体代金は一括で支払ってから当月中に解約するという流れになります。

本体代金 + 事務手数料 + 最安値のプランで合わせて10万円ほどでした。

これに契約解除の違約金で1万円ほどかかるので合わせて11万円ほどの出費になりました。

事務手続きに多少時間(1時間ほど)がかかるのとexpansysなんかから入手するよりコストはかさみますが、docomo系のmvnoのsimで最新端末を使いたいけど公式が単体で売ってくれない場合は(技適警察に捕まる前に)試してみるといいかもしれません。

ちなみに入手したXperia XZ Premiumは超快適です。

WPFでCefSharp(Chromiumの.NET向け実装)を使う - 4

概要

今回はSchemeHandlerについて書きます。

webアプリを組み込んだネイティブアプリでwebからネイティブに何かを通知する方法は複数ありますが、最も一般的だと思われるのがカスタムURLスキームを利用する方法です。

CefにもカスタムURLスキームを利用してブラウザから通知を受ける仕組みが提供されています。

カスタムURLスキームについて

hoge://fugafuga

みたいなやつです。

スマホのブラウザからgoogle map開いたりアプリストア開いたりできるのはこれのおかげです。

OSにデフォルトで定義されている挙動もありますが、Cefではその前の段階で挙動をハンドリングすることができます。

ISchemeHandlerFactoryについて

カスタムURLスキームのハンドリングにはISchemeHandlerではなくIResourceHandlerを利用します。

URLは<scheme>://<host>/<path&query>となっていて、スキームごとにCefにハンドラを登録することができるのですが、直接ハンドラを登録するのではなくて、スキームのハンドラのファクトリをプロパティに持つカスタムスキームクラスをCefSettingsに登録する必要があって、手順が少し複雑です。

カスタムスキームとして"sample"を利用する場合には↓のようなコードになります。(sample://hogeみたいなurlに遷移しようとするとハンドラが呼び出される)

var CefSettings = new CefSettings();
var customScheme = new CefCustomScheme();
customScheme.SchemeName = "sample";
customScheme.SchemeHandlerFactory = new SchemeHandlerFactory();
cefSettings.RegisterScheme(customScheme);
Cef.Initialize(cefSettings);

ISchemeHandlerFactoryの実装

ISchemeHandlerFactoryは、リクエストがあったときにそれに対応するハンドラを生成するCreateメソッドのみを実装すればOKです。

urlのホスト部で大きく処理を変えることが多いはずなので、ホスト部の処理ごとにハンドラを用意しておくのがいいと思います。

class SchemeHandlerFactory : ISchemeHandlerFactory
{
    public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
    {
        return new SchemeHandler();
    }
}

SchemeHandlerについて

カスタムスキームのハンドリングするためのSchemeHandlerやそのインターフェースであるISchemeHandlerといったものは存在せず、IRequestHandlerを実装したクラスをカスタムスキームのハンドラとして利用します。

ありがたいことにIResourceHandlerにはデフォルトの実装をもったResourceHandlerクラスが存在するので、それを拡張してハンドラを実装しましょう。

実装する必要がある個所はProcessRequestAsyncのみです。

class SchemeHandler : ResourceHandler
{
    public override bool ProcessRequestAsync(IRequest request, ICallback callback)
    {
        // 何かrequest.Urlを見たりして処理する
        return false;
    }
}

falseを返しておけばリクエストがキャンセルされるので基本的にfalseを返しておけばいいと思います。

7/18追記

内部でasyncな処理とかをしてる時はfalseを返すと即座に処理が中断されちゃうので、trueを返したうえでasyncな処理が終わったタイミングでcallback.Cancel()を呼ぶのが正しいようです(リクエストをキャンセルする場合)。

WPFでCefSharp(Chromiumの.NET向け実装)を使う - 3

概要

今回はIRequestHandlerについて書きます。

IRequestHandlerはその名の通りブラウザのリクエストをハンドリングするためのインタフェースなのですが、やたらと定義すべきメソッドが多いにもかかわらずデフォルト実装がありません。

幸いコメントは充実しているのとサンプル実装は用意されているので、それを基に何ができるのか検証します。

IRequestCallbackについて

定義を読んでいくと、IRequestCallbackのContinueを呼べだとかCancelを呼べだとか書いてある箇所が出てきます。

IRequestHandlerの説明に移る前に、IRequestCallbackが何者なのかについても調べておきます。

定義

void Continue(bool allow)

引数としてtrueを渡すとリクエストの処理を継続して、falseを渡すとキャンセルされます。

void Cancel()

リクエストがキャンセルされます。

Continue(false)Cancel()の違いは何なんだろうか……

IRequestHandlerで定義すべきメソッド

定義

bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool isRedirect)

ブラウザの遷移前に呼び出されるメソッドです。

以下コメントの直訳(間違ってても許して)

ナビゲーションが許可された場合、IWebBrowser.FrameLoadStartIWebBrowser.FrameLoadEndが呼び出される。
ナビゲーションがキャンセルされた場合、IWebBrowser.LoadErrorイベントが発火し、イベントの引数としてCefErrorCode.Abortedが渡される。

引数は型との仮引数の名前でどんなものか想像つくので省略しますが返り値がぱっと見だとよくわからないのでこれも直訳してみます。

trueを返すとナビゲーションがキャンセルされる。
falseを返すとナビゲーションの進行を許可する。

とのことなので、デフォルトの挙動としてはfalseを返し、何か画面遷移を止めたい場合のみtrueを返すことになるはずです。

bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture)

新しいタブやブラウザでurlを開こうとしたときに呼び出されるメソッドです。

新しいブラウザやタブで開くのが望ましい場合に、OnBeforeBrowseが呼び出される前にUIスレッドで呼び出される。
“新しいブラウザやタブで開くのが望ましい場合"とは中央ボタンのクリックやCtrl + 左クリック、クロスオリジンのナビゲーションがあったときのことを指す。

これの返り値もOnBeforeBrowseと同様に、falseを返すとナビゲーション続けてtrueを返すとナビゲーションをキャンセルします。

これはデフォルト値どうするか迷いますが(アプリに組み込むWebViewで新しいタブとか開かれたくない気がする)、サンプル実装だとfalseを返しているようです。

bool OnCertificateError(IWebBrowser browserControl, IBrowser browser, CefErrorCode errorCode, string requestUrl, ISslInfo sslInfo, IRequestCallback callback)

ダメなSSLのときに呼ばれるメソッドです。

不正なSSL証明書のリクエストをハンドリングするために呼び出される。
trueを返してIRequestCallback.Continueを呼び出すことで処理を継続させることができる。
CefSettings.IgnoreCertificateErrorsを使うとこのメソッドを呼ばずにすべてのsslを許可することもできる。
falseを返すと即座にリクエストがキャンセルされる。

サンプル実装のコメントには、このメソッド実装したくないならfalse返しとけばいいよって書いてあるので基本false返すようにしとけばいいはずです。

void OnPluginCrashed(IWebBrowser browserControl, IBrowser browser, string pluginPath);

プラグインがクラッシュしたときに呼ばれるメソッドです。

空の実装でも問題なさそうです。

CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)

リクエストが投げられる前に呼ばれるメソッドです。

リソースのリクエスト前に呼ばれる。
asyncで処理したいならCefReturnValue.ContinueAsyncを返してIRequestCallback.ContinueIRequestCallback.Cancelを呼ぶ。

引数で受け取ったrequestは変更可能となっているので、このメソッドの中でリクエストにヘッダを追加したりできます。

CefReturnValue.Cancelを返すことでリソースのロードをキャンセルすることができます。

bool GetAuthCredentials(IWebBrowser browserControl, IBrowser browser, IFrame frame, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback)

ブラウザがユーザに認証情報を要求するときに呼ばれるらしいです(Basic認証とか?)。

IAuthCallbackのContinueがユーザ名とパスワードを引数として受け取れるので、trueを返しつつIAuthCallback.Continueを呼ぶことで処理を継続することができそうです。

falseを返すとリクエストがキャンセルされます。

bool OnSelectClientCertificate(IWebBrowser browserControl, IBrowser browser, bool isProxy, string host, int port, X509Certificate2Collection certificates, ISelectClientCertificateCallback callback)

受け取った鍵の認証局を選ぶときに呼ばれるっぽいです。

trueを返しつつISelectClientCertificateCallback.Selectを呼べば手動で選べるけどfalseを返しておけば自動で選んでくれるみたいなのでfalseを返しておけばいいはずです。

void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status)

予期せぬ理由で描画が止まってしまったときに呼び出されます。

どんなときかよくわからなかったのでCefTerminationStatusを見てみると、終了コードが0以外だった場合とタスクマネージャからKillされた場合とせぐふぉの場合って書いてました。

bool OnQuotaRequest(IWebBrowser browserControl, IBrowser browser, string originUrl, Int64 newSize, IRequestCallback callback)

webkitStorageInfo.requestQuotaが呼ばれたときに呼び出されるようです。

false返してキャンセルしておけば基本問題ないでしょう。

void OnResourceRedirect(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response, ref string newUrl)

302とかが返ってきたときに呼び出されます。

requestは編集不可ですがリダイレクト先のURLは書き換えられるみたいです。

bool OnProtocolExecution(IWebBrowser browserControl, IBrowser browser, string url)

ブラウザで処理できないプロトコルが指定されたときに呼び出されます(多分mailtoとか)。

trueを返すとOSにそのプロトコルのハンドリングを任せて、falseを返すと何もしないみたいです。

void OnRenderViewReady(IWebBrowser browserControl, IBrowser browser)

UIスレッドの準備ができたとき?に呼ばれます。

多分空の実装でOKです。

bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response)

レスポンスを受信したときに呼ばれます。

trueを返すとリクエストを再送することができて、requestの編集も可能です。

falseを返すとそのまま処理を続けます。

IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response)

IResponseFilerを実装したオブジェクトを返すことでレスポンスのstreamをフィルタリングできるようです。

void OnResourceLoadComplete(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength)

リソースのロードが完了したときに呼ばれます。

まとめ

  • リクエストの送信前からレスポンスの受信完了まで様々なタイミングで処理を挟むことができる
  • boolを返すメソッドは基本falseで問題なし
  • voidを返すメソッドは基本空の実装で問題なし

次回はCustom URL Schemeを扱うためのハンドラについて書きます。

WPFでCefSharp(Chromiumの.NET向け実装)を使う - 2

経緯

前回の続きです。

今回は具体的なWebBrowserとの比較として、以下の機能の実装方法について検証します。

  • UserAgentの設定
  • ページ遷移のハンドリング
  • Cookieの操作

UserAgentの設定

WebBrowserの場合

urlmon.dllUrlMkSetSessionOptionを利用すると実現できます→参考

難しいことはないんですがアンマネージドなコードを触るのがちょっともやっとします。

Cefの場合

CefSharp.Wpf.ChromiumWebBrowserインスタンスを生成する前に、Cef.Initialize(cefSettings)を呼び出すことでUserAgent等の設定を行うことができます。

この関数の引数であるCefSettingsは他にもいろいろ設定できる項目があるので後々検証します。

今回はUserAgentを設定したいだけなので、UIスレッドでUserAgentを指定したCefSettingsを使ってCefをInitializeするだけです。

public static void Init()
{
    var cefSettings = new CefSettings
    {
        UserAgent = "CefSample",
    };
    Cef.Initialize(cefSettings);
}

f:id:ghken:20170426233050p:plain

ちゃんとUserAgentがCefSampleになってます。

ページ遷移のハンドリング

WebBrowserの場合

WebBrowser.Navigating, WebBrowser.Navigatedイベントがあるので、それぞれ遷移前、遷移後の処理をハンドリングすることができます。

また、Navigatingのイベントハンドラに第二引数として渡されるNavigatingCancelEventArgsのCancelプロパティをtrueにしておくことで、遷移をキャンセルさせることができます。

Cefの場合

ChromiumWebBrowser.FrameLoadStart, ChromiumWebBrowser.FrameLoadEndイベントがあるので、それぞれ遷移開始、遷移後の処理をハンドリングすることができます。

ただ、このFrameLoadStartイベントでは遷移をキャンセルすることができず、リクエストの書き換えや遷移のキャンセルを制御するにはChromiumWebBrowser.RequestHandlerプロパティを実装する必要があります。

RequestHandlerは多機能なので別途検証を行います(今回は行いません)。

Browser = new ChromiumWebBrowser();
Browser.FrameLoadStart += (sender, e) =>
{
    var uri = new Uri(e.Url);
    if (uri.Host == "www.google.co.jp")
    {
        MessageBox.Show("www.google.co.jpのページに遷移します");
    }
};

みたいなことができます。

Cookieの操作

実際にやりたかったのは"クッキーを削除する"ボタンの実装なので、それをどうやって実現できそうかを見ていきます。

WebBrowserの場合

Cookieをまとめて削除するような機能はなさそうです(あったら教えてください)。

一応アンマネージドなコードを触ってInternetSetOptionでいろいろやれば既存のCookieを無視したブラウザを立ち上げることは可能なのですが、

直接消すわけではないので"クッキーを削除する"とは違う挙動になってしまいます。(オプションを無効化して再起動すると前のCookieが残ってる)

WebBrowserにはDocumentプロパティが存在して、HTMLにjsを挿入することができるので、そのjsでそのドメインCookieのExpireをいじることは可能なのですが、

すべてのページのCookieを削除することは難しそうです。

Cefの場合

Cookieを操作するためのCookieManagerクラスが用意されていて、Cef.GetGlobalCookieManager()でCookieManagerを取得することができます。

CookieManagerには特定のUrlのCookieを消すためのDeleteCookiesが実装されているので、一つだけ消す場合であればそれを呼べばOKです。

複数のドメインCookieを処理するためにはアプローチを少し変える必要があって、ICookieVisitorを実装したクラスを用意して、そのインスタンスをCookieManagerのVisitAllCookiesに渡す必要があります(VisitUrlCookiesってのもあったのでドメインごとに消すとかも簡単にできそう)。

またasyncに処理してくれるVisitAllCookiesAsyncというものもあって、こちらは返り値でCookieのListを返してくれるのですが、返ってきたところでどうやって消せばいいのかはよくわかりませんでした。

void ClearAllCookies()
{
    var cookieManager = Cef.GetGlobalCookieManager();

    cookieManager.VisitAllCookies(new CookieEater());
}

class CookieEater : ICookieVisitor
{
    public void Dispose()
    {
    }

    public bool Visit(Cookie cookie, int count, int total, ref bool deleteCookie)
    {
        deleteCookie = true;
        return true;
    }
}

まとめ

  • CefのInitializeにCefSettingsを渡すといろいろできる(UserAgentとかCacheのパスとか)
  • FrameLoadStartとFrameLoadEndでページのロード開始とページのロード完了のイベントが取れる
    • リクエスト自体をいじりたいとかならRequestHandlerを実装する必要がある(また今度検証)
  • CookieManagerとCookieVisitorを使ってCookieを簡単に操作できる

今回はここまでです。

次回はRequestHandlerあたりについて書こうと思います。

WPFでCefSharp(Chromiumの.NET向け実装)を使う - 1

経緯

WPFにはデフォルトでウェブブラウザを扱うためのコンポーネントSystem.Windows.Controls.WebBrowserが用意されていて、簡単にウェブブラウザを埋め込んだアプリケーションの開発を行うことができます。

しかし、このWebBrowserコンポーネントはなかなか厄介で、以下のような問題を抱えています。

  • デフォルトでIE7互換のブラウザとして振舞う(IE11互換で動かすためにはレジストリの書き換えが必要)
  • UserAgent変えたりするのにアンマネージドなコードを触る必要がある
  • 一時ファイル類の操作がつらい(キャッシュやCookie消せないしそもそもどこにいるのかわからない)

単にウェブページを表示するだけのアプリであればWebBrowserコンポーネントで問題ないのですが、少し複雑なことをやろうとするとつらくなってきます。

なので、WebBrowser以外のWPFで利用可能なブラウザのコンポーネントを調べてみることにしました。

ちょっと分量が多くなりそうなので数回に分けて検証します。

Cefについて

CefはChromium Embedded Frameworkの略で、Chromiumをアプリケーションに組み込むためのフレームワークです。

bitbucket.org

Cefには各言語向けのラッパーがあり、その中のC#実装がCefSharpです。

github.com

CefSharpにはWPF対応のコンポーネントが用意されているので、それを利用してWebBrowserを置き換えることができるか検証してみます。

インストール

CefSharpはNuGetでインストールできるようになっているのですが注意すべきことがあります。

  • 2017/4/24現在の最新版は.NET4.5.2以上を要求する
  • Any CPUには対応していないので、プラットフォームをx86かx64にしておく(しておかないと参照がうまく追加されなくて手動で追加する必要がある?)

f:id:ghken:20170425001655p:plain

動かしてみる

適当に作ったWPFのプロジェクトのMainWindow.xaml.csにでも書いてみる。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        var browser = new CefSharp.Wpf.ChromiumWebBrowser();
        browser.Address = "https://google.co.jp";
        Content = browser;
    }
}

f:id:ghken:20170425001703p:plain

URLの遷移について

ウェブブラウザを別のURLに遷移させる際、ChromiumWebBrowser.Addressを書き換える方法と、ChromiumWebBrowser.Load(url)を呼ぶ方法があります。

基本的な挙動に変わりはありませんが、ブラウザが初期化されていないときに実行すると挙動が変わります。

Addressを書き換えた場合

ブラウザの初期化後にそのURLに遷移する

Loadを呼んだ場合

初期化されていないので何も起こらない

まとめ

  • WPFでウェブブラウザを組み込んだアプリケーションを作りたいならWebBrowserを使う方法とCefSharpがある(ほかにもあるかも)
  • プラットフォームをx86/x64にしたWPFのプロジェクトにNuGetでCefSharp.Wpfを追加することでChromiumベースのウェブブラウザコンポーネントが利用できる
  • Load(url)やAddressで遷移できるけど初期化前の挙動に注意

次回はWebBrowserの各機能との対応を検証します。