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を返しておけばいいと思います。

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の各機能との対応を検証します。