Hatena::ブログ(Diary)

酢ろぐ(ch3cooh.jp) RSSフィード


CH3COOH(酢酸)の実験室
Baseball Japan / ○○時計 for WP7 / for WM
Windows Phone 7 開発 Tips

2011-11-12

WebBrowserコントロールでレスポンスをフックしてページ遷移をキャンセルする

例えば○○社の販促アプリケーションで、○○社以外のコンテンツが閲覧できるのが問題だ。または、特定の競合先である△△社のウェブページを見せたくないという要望が上がってくるかもしれません。

WebBrowserコントロールでは、ページの遷移前にNavigatingイベントが発生します。特定の条件が満たされなかった場合など、イベントハンドラに渡されるNavigatingEventArgsオブジェクトのCancelプロパティを「True」に設定することで遷移をキャンセルすることが可能です。

テストに使用したページは、以下のように普通のリンク先が2件、2ちゃんねる関係のリンクが2件あるHTMLファイルを用意しました。

<html>
  <body>
    <a href="http://ch3cooh.jp/">CH3COOH(酢酸)の実験室</a><br />
    <a href="http://www.yahoo.co.jp/">Yahoo! JAPAN</a><br />
    <a href="http://www.2ch.net/">2ちゃんねる</a><br />
    <a href="http://hibari.2ch.net/smartphone/">
      スマートフォン@2ch掲示板</a><br />
  </body>
</html>

テスト用ページをアプリで表示させてみました。

f:id:ch3cooh393:20111113002334p:image

下記の例は、Navigatingイベントハンドラ内にてURLを判定し、2ch.netを含むページへの遷移をすべてキャンセルしています*1

using System.Text.RegularExpressions;
using Microsoft.Phone.Controls;
using System.Windows;
using System;
using System.Windows.Navigation;

namespace WebBrowserHookTest {
    public partial class MainPage : PhoneApplicationPage {
        public MainPage() {
            InitializeComponent();
        }

        protected override void OnNavigatedTo(NavigationEventArgs e) {
            webBrowser.Navigate(new Uri("http://ch3cooh.jp/files/webbrowser_linkhook.html"));
        }

        private void webBrowser_Navigating(object sender, NavigatingEventArgs e) {
            // 2ちゃんねるドメインであればページ遷移させない
            if (Regex.IsMatch(e.Uri.Host, "(?:.*\\.)2ch\\.net$")) {
                MessageBox.Show("本アプリは2chの閲覧を許可していません!");
                e.Cancel = true;
            }
        }
    }
}

ページの遷移をキャンセルし、メッセージボックスで注意を促してみました。

f:id:ch3cooh393:20111113002335p:image

JavaScriptを使ってWebBrowserコントロールを制御する

WebBrowserコントロールを使って自動ログイン機能を実装する」では、JavaScriptを使ってYahoo!Japanへのログイン機能を実装しました。何故JavaScriptのコードが実行出来るのか説明不足でしたので、後出しになってしまいましたが改めて説明したいと思います。

WebBrowser.InvokeScriptメソッドについて

その前にWebBrowser.InvokeScriptメソッドの使い方について説明しておきます。WebBrowser.InvokeScriptメソッドは、現在表示されているページにて定義されているスクリプトの名前を指定して実行するメソッドです。

InvokeScript(String)スクリプトの名前を指定して実行する
InvokeScript(String, String[])スクリプトの名前を指定して引数を与えて実行する

以下のようなHTMLファイルの場合は、「funcWithoutPram」の部分がスクリプトの名前になります。

HTML

実行するHTMLファイルを準備します。あらかじめJavaScript引数無しメソッド、1つだけ引数を受け取るメソッド、2つ引数を受け取るメソッドを用意しておきます。いずれもコールされると「None Data.」の部分を書き換えます。

<html>
    <head>
        <script type="text/javascript">
            function funcWithoutPram() {
              result.innerHTML = "called funcWithoutPram";
            }
            function funcWithPram1(parm1) {
              result.innerHTML = "called funcWithPram1(" + parm1 + ")";
            }
            function funcWithPram2(parm1,parm2) {
              result.innerHTML = "called funcWithPram2("
                + parm1 + "," + parm2 + ")";
            }
        </script>
    </head>
    <body>
    <div id="result">
        None Data.
    </div>
    </body>
</html>

このHTMLファイルを単純に表示しているだけの状態です。

f:id:ch3cooh393:20111112205354p:image

None Data.が表示されています。下に並んでいるボタンを押すと指定したスクリプトを実行するようにします。

C#

コードです。OnNavigatedToメソッドで先ほどのHTMLページへ遷移させています。

WebBrowser.InvokeScriptメソッドを使う前には、XAMLかコード上かのいづれかで必ずIsScriptEnabledプロパティがTrueに設定していることを確認してください。

IsScriptEnabledプロパティが無効のままInvokeScriptメソッドを実行しようとすると、例外(System.SystemException)が発生してしまいます。「An unknown error has occurred. Error: 80020006.」としかメッセージが出ないので原因に気付きにくいので気を付けてください。

using System;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;

namespace WebBrowserScriptTest {
    public partial class MainPage : PhoneApplicationPage {
        // コンストラクター
        public MainPage() {
            InitializeComponent();
        }

        protected override void OnNavigatedTo(NavigationEventArgs e) {
            // テスト用ページへ遷移する
            var url = "http://ch3cooh.jp/files/webbrowser_script1.html";
            webBrowser.Navigate(new Uri(url));
        }

        private void button1_Click(object sender, System.Windows.RoutedEventArgs e) {
            // 引数無しのJavaScriptを実行
            webBrowser.InvokeScript("funcWithoutPram");
        }

        private void button2_Click(object sender, System.Windows.RoutedEventArgs e) {
            // 引数が1つのJavaScriptを実行
            webBrowser.InvokeScript("funcWithPram1", "引数1");
        }

        private void button3_Click(object sender, System.Windows.RoutedEventArgs e) {
            // 引数が2つのJavaScriptを実行
            webBrowser.InvokeScript("funcWithPram2", "引数1", "引数2");
        }
    }
}

ボタンをタップすると定義されたJavaScriptメソッドを実行します。

下図は、HTMLファイルに定義されたfuncWithPram1メソッドとfuncWithPram2メソッド引数を渡して実行してみたスクリーンショットです。

f:id:ch3cooh393:20111112210231p:image

何故JavaScriptのコードが実行出来るのか

WebBrowser.InvokeScriptメソッドは、既に定義済みのスクリプトを実行するメソッドであるということを理解して頂けたと思います。本題の何故JavaScriptのコードが実行出来るのかを説明したいと思います。

前回ログイン機能でのコードをひとつピックアップしてみました。

  webBrowser1.InvokeScript("eval", "document.forms['login_form'].submit();");

JavaScriptにはeval関数というものがあります。eval関数引数として実行したいJavaScriptのコードを文字列で受けます。

上記のInvokeScriptメソッドは、「eval」というスクリプトに対して、「document.forms['login_form'].submit();」というJavaScriptコードを引数として渡して実行します。

このevalメソッドを使うことでどんなことをが出来るのか?ざっと書き出してみました。

WebBrowserコントロールで表示しているページのタイトルを取得する
// 表示しているタイトルを取得する
var title = webBrowser.InvokeScript("eval", "document.title");
WebBrowserコントロールで表示しているページのURLを取得する
// 表示しているURLを取得する
var strURL = webBrowser.InvokeScript("eval", "document.URL");
WebBrowserコントロールで表示しているテキストボックスの値を取得/設定する

以下のようなパスワードボックス(パスワード入力欄)を持つHTMLがあるとします。

<input id="password" type="password" />

このパスワード入力欄は「password」というIDを持っています。IDから要素を取得するには、JavaScriptのdocument.getElementByIdメソッドを使います。取得した要素のvalueプロパティを取得することで、現在何が入力されているのかを知ることが出来ます。

// テキストボックスの値を取得する
var value = webBrowser.InvokeScript("eval", "document.getElementById('password').value");

逆に設定したい場合もまずは対象となる要素を取得します。document.getElementByIdメソッドを使って、取得した要素のvalueプロパティに値を設定します。

    // テキストボックスに値を設定する(戻り値は設定した値)
    var result = webBrowser.InvokeScript("eval", "document.getElementById('password').value = 'hogehgoe'");

上記のコードを任意のタイミングで実行すると「hogehoge」がテキストボックスに反映されます。正しく実行されていれば、InvokeScriptメソッド戻り値を受けた変数resultも「hogehoge」を含んでいると思います。


追記

今回の記事ではXAMLはたぶん要らないと思うけど、一応掲載しておきます。

<phone:PhoneApplicationPage 
    x:Class="WebBrowserScriptTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" Text="SOFTBUILD" 
                       Style="{StaticResource PhoneTextNormalStyle}"/>
            <TextBlock x:Name="PageTitle" Text="Script Test" Margin="9,-7,0,0" 
                       Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
        	<phone:WebBrowser Margin="0,0,0,100" Name="webBrowser" IsScriptEnabled="True" />
        	<Grid Height="100" VerticalAlignment="Bottom" >
        		<Grid.ColumnDefinitions>
        			<ColumnDefinition Width="0.33*"/>
        			<ColumnDefinition Width="0.33*"/>
        			<ColumnDefinition Width="0.34*"/>
        		</Grid.ColumnDefinitions>
                <Button x:Name="button1" Content="Parm無し" Grid.Column="0" 
                        d:LayoutOverrides="Width" FontSize="21.333" Click="button1_Click"/>
        		<Button x:Name="button2" Content="Parm有り1" Grid.Column="1" 
                        d:LayoutOverrides="Width" FontSize="21.333" Click="button2_Click"/>
        		<Button x:Name="button3" Content="Parm有り2" Grid.Column="2" 
                        d:LayoutOverrides="Width" FontSize="21.333" Click="button3_Click"/>
        	</Grid></Grid>
    </Grid>    
</phone:PhoneApplicationPage>

*1正規表現が合っているかは微妙