WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

WebBrowser の拡張

by WebSurfer 2012年7月1日 18:04

先の記事 SHDocVw.dll と AxSHDocVw.dll の作り方と使い方 では、ActiveX の WebBrowser コントロール (shdocvw.dll) をホストする、AxHost クラス から派生するラッパーコントロールを Visual Studio で生成し、それ使って NewWindow2 イベントを利用する例を書きました。

それと比較するために、.NET Framework の WebBrowser(これも shdocvw.dll のマネージラッパー)を拡張して同様なことを行うコードを書いてみました。

かなり面倒で、最初は COM の相互運用の知識がなかったのでお手上げ状態でした。あちこちググって調べて、動くようになるまで 3 日ぐらいかかりました。(笑)

詳しくは以下のコードとそのコメントを参照してください。参考にしたページの一覧も書いておきます。(手抜きですみません)

WebBrowser.CreateSink メソッド

Extended .NET 2.0 WebBrowser Control

COM相互運用機能の利用

COM相互運用機能の利用 - パート2

Microsoft .NET/COM の移行と相互運用性

COM ラッパー

ランタイム呼び出し可能ラッパー

COM 相互運用性 - 第 1 部 : C# クライアント チュートリアル

方法: COM ソースによって発生したイベントを処理する

.NET の WebBrowser を拡張したクラス

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Reflection;

namespace WebBrowserExtended
{
  public class MyWebBrowser : WebBrowser
  {
    // WebBrowser の AxtiveX への参照
    private IWebBrowser2 axIWebBrowser2;

    // WebBrowser の AxtiveX が作成されたとき呼び出される
    [PermissionSet(SecurityAction.LinkDemand, 
      Name = "FullTrust")]
    protected override void 
      AttachInterfaces(object nativeActiveXObject)
    {
      this.axIWebBrowser2 = 
        (IWebBrowser2)nativeActiveXObject;
      base.AttachInterfaces(nativeActiveXObject);
    }

    [PermissionSet(SecurityAction.LinkDemand, 
      Name = "FullTrust")]
    protected override void DetachInterfaces()
    {
      this.axIWebBrowser2 = null;
      base.DetachInterfaces();
    }

    public object Application
    {
      get
      {
        if ((this.axIWebBrowser2 == null))
        {
          throw new 
            AxHost.InvalidActiveXStateException(
              "Application",
              AxHost.ActiveXInvokeKind.PropertyGet);
        }

        // この Application プロパティは COM の
        // マネージラッパー
        return this.axIWebBrowser2.Application;
      }
    }

    public bool RegisterAsBrowser
    {
      get
      {
        if ((this.axIWebBrowser2 == null))
        {
          throw new 
            AxHost.InvalidActiveXStateException(
              "RegisterAsBrowser",
              AxHost.ActiveXInvokeKind.PropertyGet);
        }

        // この RegisterAsBrowser プロパティは
        // COM のマネージラッパー
        return this.axIWebBrowser2.RegisterAsBrowser;
      }
      set
      {
        if ((this.axIWebBrowser2 == null))
        {
          throw new 
            AxHost.InvalidActiveXStateException(
              "RegisterAsBrowser",
              AxHost.ActiveXInvokeKind.PropertySet);
        }

        // この RegisterAsBrowser プロパティは
        // COM のマネージラッパー
        this.axIWebBrowser2.RegisterAsBrowser = value;
      }
    }

    // シンクオブジェクトへの参照
    private MyWebBrowserEventSink sink;

    // HTTP 通信の cookie とは関係ないので注意
    private AxHost.ConnectionPointCookie cookie;

    // シンクをサブスクライバ・リストに追加
    [PermissionSetAttribute(SecurityAction.LinkDemand, 
      Name="FullTrust")]
    protected override void CreateSink() 
    {
      base.CreateSink();

      if ((this.axIWebBrowser2 == null)) 
      {
        throw new AxHost.InvalidActiveXStateException(
            "CreateSink",
            AxHost.ActiveXInvokeKind.MethodInvoke);
      }

      this.sink = new MyWebBrowserEventSink(this);
      this.cookie = new AxHost.ConnectionPointCookie(
                          this.axIWebBrowser2,
                          this.sink, 
                          typeof(DWebBrowserEvents2));
    }

    // シンクのサブスクライブを解除
    [PermissionSetAttribute(SecurityAction.LinkDemand, 
      Name="FullTrust")]
    protected override void DetachSink() 
    {
      if (cookie != null) 
      {
        this.cookie.Disconnect();
        this.cookie = null;
      }
      base.DetachSink();
    }

    // NewWindow2 イベントの定義
    public event NewWindow2EventHandler NewWindow2;

    // .NET 側の NewWindow2 イベントを発動するメソッド
    protected virtual void 
      OnNewWindow2(NewWindow2EventArgs e) 
    {
      if ((this.NewWindow2 != null)) 
      {
        this.NewWindow2(this, e);
      }
    }

    // コネクションポイントからの呼び出しを受け取る
    // クライアント・シンクのクラス定義
    [ClassInterface(ClassInterfaceType.None)]
    public class MyWebBrowserEventSink :
      StandardOleMarshalObject, DWebBrowserEvents2
    {
      private MyWebBrowser browser;

      public MyWebBrowserEventSink(MyWebBrowser browser)
      {
        this.browser = browser;
      }

      // COM ソースから発生したイベントから呼び出される
      // メソッド
      public void 
        NewWindow2(ref object ppDisp, ref bool cancel)
      {
        NewWindow2EventArgs e = 
          new NewWindow2EventArgs(ppDisp, cancel);

        // .NET 側の NewWindow2 イベントを発動
        this.browser.OnNewWindow2(e);

        ppDisp = e.PpDisp;
        cancel = e.Cancel;
      }
    }
  }

  // NewWindow2 イベントのハンドラのデリゲート
  public delegate void NewWindow2EventHandler(object sender, 
    NewWindow2EventArgs e);

  // NewWindow2 イベントハンドラ引数のクラス定義
  public class NewWindow2EventArgs : EventArgs
  {        
    public object PpDisp { get; set; }
    public bool Cancel { get; set; }
        
    public NewWindow2EventArgs(object ppDisp, bool cancel)
    {
      this.PpDisp = ppDisp;
      this.Cancel = cancel;
    }
  }

  // DWebBrowserEvents2 インターフェイスの NewWindow2 メ
  // ソッド、IWebBrowser2 インターフェイスの Application
  // プロパティと RegisterAsBrowser プロパティをインポー
  // ト(つまり、マネージラッパーをコンパイル時に生成)。
  // ComImport, InterfaceType, Guid 指定は必須らしい。
  [ComImport, 
  InterfaceType(ComInterfaceType.InterfaceIsIDispatch), 
  Guid("34A715A0-6587-11D0-924A-0020AFC7AC4D")]
  public interface DWebBrowserEvents2
  {
    [DispId(0xfb)]
    void NewWindow2(
      [In, Out, MarshalAs(UnmanagedType.IDispatch)]
      ref object ppDisp, 
      [In, Out, MarshalAs(UnmanagedType.VariantBool)] 
      ref bool Cancel);
  }

  [ComImport, 
  Guid("D30C1661-CDAF-11D0-8A3E-00C04FC9E26E"),
  InterfaceType(ComInterfaceType.InterfaceIsIDispatch), ]
  public interface IWebBrowser2
  {
    [DispId(200)]
    object Application
    {
      [return: MarshalAs(UnmanagedType.IDispatch)]
      get;
    }

    [DispId(0x228)]
    bool RegisterAsBrowser
    {
      get;
      set;
    }
  }
}

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WebBrowserExtended
{
  public partial class Form1 : Form
  {
    private MyWebBrowser browser;
        
    public Form1()
    {
      browser = new MyWebBrowser();
            
      InitializeComponent();
                        
      browser.Dock = DockStyle.Fill;
      this.Controls.Add(browser);
      browser.NewWindow2 += 
        new NewWindow2EventHandler(browser_NewWindow2);
    }

    private void button1_Click(object sender, EventArgs e)
    {
      browser.Navigate(
        "http://msdntestnew/159-HyperLinkToPdf.aspx");
    }

    private void browser_NewWindow2(object sender, 
      NewWindow2EventArgs e)
    {
      Form1 frmWB = new Form1();

      // WebBrowser.AttachInterfaces メソッドは Visible
      // プロパティを true にすると呼び出される。なので、
      // ここで設定しないと RegisterAsBrowser プロパティ、
      // Application プロパティで例外がスローされてしまう。
      frmWB.Visible = true;

      frmWB.browser.RegisterAsBrowser = true;
      e.PpDisp = frmWB.browser.Application;
    }
  }
}

Tags:

.NET Framework

削除の際の @original_xxx はどこから取得?

by WebSurfer 2012年6月24日 00:21

楽観的同時実行制御を有効にした時、クエリの WHERE 句には [xxx] = @original_xxx というような条件が設定されます。SqlDataSource と DetailsView などを使った場合の削除操作の際、@original_xxx はどこから取得されるでしょうか?

DetailsView での削除操作

上の画像の例では、ItemTemplate に Label を配置して文字列を表示するようにしていますが、削除操作を行う場合、@original_xxx はどこから取得されるかというと、ItemTemplate に配置した Label の Text プロパティからです。

Label に文字列を表示しているのにブラウザの表示で改行されているのは DetailsView.PreRender イベントのハンドラで Environment.NewLine を "<br />" に置き換えているからです。

従って、このように表示される文字列をプログラムで書き換えてしまうと、DELETE クエリの WHERE 句で [xxx] = @original_xxx が成立しないので、削除に失敗します。(これは DetailsView に限らず、GridView などでも同じです)

ちなみに、Text='<%# Bind("text") %>' の Bind を Eval に変更すると、 DetailsViewDeleteEventArgs.Values["text"] は null になります。その 結果、削除ボタンクリックで InvalidOperationException がスローされます。

というわけで、楽観的同時実行制御を行って、この例のように Environment.NewLine を "<br />" に置き換えて表示するような場合、削除操作直前に、@original_text に代入される文字列を元に戻すしかなさそうです。

それは、以下のコードのように、SqlDataSource.Deleting イベントのハンドラで行うのがよさそうです。

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  // ウィザードで作っていくと Label も Text='<%# Bind("text") %>' 
  // となる。なぜなら、@original_text に代入される文字列は Text 
  // プロパティから取得するから。それゆえ、以下のようにプログラム
  // で表示する文字列を書き換えると [text] = @original_text が 
  // true にならず、削除に失敗する。
  // Text='<%# Bind("text") %>' の Bind を Eval に変更すると、
  // DetailsViewDeleteEventArgs.Values["text"] は null になる。
  // 結果、[削除]ボタンクリックで InvalidOperationException
  // がスローされる。
  protected void DetailsView1_PreRender(object sender, 
    EventArgs e)
  {
    Control control = 
      ((DetailsView)sender).FindControl("Label1");

    if (control != null)
    {
      Label label = (Label)control;
      label.Text = 
        label.Text.Replace(Environment.NewLine, "<br />");
    }
  }

  // 検証用
  protected void DetailsView1_ItemDeleting(object sender, 
    DetailsViewDeleteEventArgs e)
  {
    // Environment.NewLine が <br /> に変更されて
    // いることが確認できる。
    string text = (string)e.Values["text"];
  }

  protected void SqlDataSource1_Deleting(object sender, 
    SqlDataSourceCommandEventArgs e)
  {
    // 削除を可能するには @original_text に代入される文字
    // 列を元に戻すしかない。
    string text = 
      (string)e.Command.Parameters["@original_text"].Value;
    text = text.Replace("<br />", Environment.NewLine);
    e.Command.Parameters["@original_text"].Value = text;
  }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
      ConflictDetection="CompareAllValues" 
      ConnectionString="<%$ ConnectionStrings:MyDB %>" 
      DeleteCommand=
        "DELETE FROM [Table3] 
        WHERE [id] = @original_id AND [text] = @original_text" 
      InsertCommand=
        "INSERT INTO [Table3] ([text]) VALUES (@text)" 
      OldValuesParameterFormatString="original_{0}" 
      SelectCommand="SELECT [id], [text] FROM [Table3]" 
      UpdateCommand=
        "UPDATE [Table3] 
        SET [text] = @text 
        WHERE [id] = @original_id AND [text] = @original_text" 
      OnDeleting="SqlDataSource1_Deleting">
      <DeleteParameters>
        <asp:Parameter Name="original_id" Type="Int32" />
        <asp:Parameter Name="original_text" Type="String" />
      </DeleteParameters>
      <InsertParameters>
        <asp:Parameter Name="text" Type="String" />
      </InsertParameters>
      <UpdateParameters>
        <asp:Parameter Name="text" Type="String" />
        <asp:Parameter Name="original_id" Type="Int32" />
        <asp:Parameter Name="original_text" Type="String" />
      </UpdateParameters>
    </asp:SqlDataSource>

    <asp:DetailsView ID="DetailsView1" 
      runat="server" 
      AllowPaging="True" 
      AutoGenerateRows="False" 
      DataKeyNames="id" 
      DataSourceID="SqlDataSource1" 
      Width="550px" 
      OnPreRender="DetailsView1_PreRender" 
      OnItemDeleting="DetailsView1_ItemDeleting">
      <Fields>
        <asp:BoundField DataField="id" 
          HeaderText="id" 
          InsertVisible="False" 
          ReadOnly="True" 
          SortExpression="id" />
        <asp:TemplateField HeaderText="text" 
          SortExpression="text">
          <EditItemTemplate>
            <asp:TextBox ID="TextBox1" 
              runat="server" 
              Rows="15" 
              Text='<%# Bind("text") %>' 
              TextMode="MultiLine" 
              Width="500px">
            </asp:TextBox>
          </EditItemTemplate>
          <InsertItemTemplate>
            <asp:TextBox ID="TextBox1" 
              runat="server" 
              Rows="15" 
              Text='<%# Bind("text") %>' 
              TextMode="MultiLine" 
              Width="500px">
            </asp:TextBox>
          </InsertItemTemplate>
          <ItemTemplate>
            <asp:Label ID="Label1" 
              runat="server" 
              Text='<%# Eval("text") %>'>
            </asp:Label>
          </ItemTemplate>
        </asp:TemplateField>
        <asp:CommandField 
          ShowDeleteButton="True" 
          ShowEditButton="True" 
          ShowInsertButton="True" />
      </Fields>
    </asp:DetailsView>
  </div>
  </form>
</body>
</html>

Tags:

ASP.NET

SHDocVw.dll と AxSHDocVw.dll の作り方と使い方

by WebSurfer 2012年6月23日 16:11

ActiveX の WebBrowser コントロール (SHDocVw.dll) を .NET Windows Forms アプリで扱おうとしてかなりハマったので、再びそのようなことがないよう備忘録を残しておきます。

AxSHDocVw.dll を使用した Windows Forms アプリ

.NET Framework 2.0 以降では、SHDocVw.dll のマネージラッパーである WebBrowser コントロール が提供されていますので、.NET Windows Forms アプリで SHDocVw.dll を扱うことはあまりないかもしれませんが、公開されてないイベントを利用したい場合などは困ります。

マネージラッパーの WebBrowser を拡張する方法(Extended .NET 2.0 WebBrowser Control 参照)もあるようですが、今回の目的(target="_blank" のリンクをクリックすると、セッションを維持して、上の画像のように別の Form1 に表示)程度なら、直接 SHDocVw.dll を扱ったほうが簡単そうに思えたので、そうしてみました。

以下の記事を読む前に、shdocvw.dll コンポーネントに関する予備知識として、MSDN ライブラリの Internet Explorer のアーキテクチャ に目を通しておくことをお勧めします。

まず、Windows Forms アプリで ActiveX コントロールをホストするには、AxHost クラス から派生するラッパーコントロールを生成する必要があります。

それには、SDK に含まれている Aximp.exe (Windows フォーム ActiveX コントロールインポーター) を使って、ActiveX コントロール用の COM タイプライブラリに属する型定義を Windows フォームコントロールに変換します。

具体的には、以下の画像のようにコマンドプロンプトから aximp.exe を起動し、shdocvw.dll(ActiveX コントロール用の COM タイプライブラリ。即ち IE のコンポーネント)から、Microsoft Web Browser コントロール用の共通言語ランタイムプロキシ (SHDocVw.dll) および Windows フォームプロキシ (AxSHDocVw.dll) を作成します。

コマンドプロンプトから aximp.exe を起動し SHDocVw.dll と AxSHDocVw.dll を生成

紛らわしいのが、IE のコンポーネントの shdocvw.dll と、Aximp.exe が生成する共通言語ランタイムプロキシ SHDocVw.dll の名前が同じという点です。名前が同じなだけで、中身は別物ですので注意してください。実は、自分が最初にハマったのがここです。(笑)

共通言語ランタイムプロキシの SHDocVw.dll を生成するので、IE のコンポーネントの shdocvw.dll が存在するフォルダで作業すると、「AxImp Error: 出力ファイル 'SHDocVw.dll' への書き込みエラーです。」というエラーが出てうまくいきません。

オプションで /out: e:\AxSHDocVw.dll というように、プロキシを出力したいフォルダを指定し(この例では e:\)、ファイル名を AxSHDocVw.dll とすれば、以下の画像のように期待通りプロキシが生成されます。なお、この例では /source オプションを追加しているので、AxSHDocVw.dll のソースと PDB ファイルが追加されています。

生成された SHDocVw.dll と AxSHDocVw.dll

以上で Microsoft Web ブラウザコントロール用の共通言語ランタイムプロキシ (SHDocVw.dll) および Windows フォームプロキシ (AxSHDocVw.dll) が作成できました・・・が、実は、このように手動で作る必要はなかったのでした。(汗)

Visual Studio のウィザードで作ることが可能で、その方法は以下の通りです。

まず、Visual Studio のツールボックスに Microsoft Web Browser を追加します。

Visual Studio のツールボックスに Microsoft Web Browser を追加

ツールボックスの空白部分をクリックして出てくるダイアログで[アイテムの選択(I)...]をクリックして[ツールボックス アイテムの選択]ダイアログボックスを表示します。

上の画像のように、[COM コンポーネント]タブをクリックし、一覧から Microsoft Web Browser を探してチェックを入れます。

[OK]をクリックすると、ツールボックスに WebBrowser コントロールが追加されます("Microsoft Web Browser" というテキストで表示されます)。

ツールボックスから Microsoft Web Browser を Form にドラッグ&ドロップすると自動的に SHDocVw.dll と AxSHDocVw.dll 作って、参照設定に加えてくれます。(下の「2012/7/1 注記追加」を参照)

参照設定に追加された AxSHDocVw と SHDocVw

あとは、以下のような NewWindow2 イベントのハンドラのコードを記述すれば、target="_blank" のリンクをクリックすると新たに Form1 生成されて表示され、その中の axWebBrowser に pdf が表示されます。また、セッションも引き継がれます(IE7 以前のブラウザはダメかも・・・未検証です)。

private void axWebBrowser1_NewWindow2(object sender, 
    AxSHDocVw.DWebBrowserEvents2_NewWindow2Event e)
{
    // target="_blank" のリンクをクリックしたとき、
    // 以下のコードがないと、新たに IE が開きそこ
    // に pdf が表示される。セッションは切れる。

    // 以下のコードがあると、新たに Form1 が表示さ
    // れ、その中の axWebBrowser に pdf が表示され
    // る。セッションは引き継がれる。

    Form1 frmWB = new Form1();
    frmWB.axWebBrowser1.RegisterAsBrowser = true;
    e.ppDisp = frmWB.axWebBrowser1.Application;
    frmWB.Visible = true;
}

----- 2012/7/1 注記追加 -----

Visual Studio 2010 のツールボックスから Microsoft Web Browser を Form にドラッグ&ドロップすると自動的に生成されるプロキシ(ラッパー)の名前は、Interop.SHDocVw.dll と AxInterop.SHDocVw.dll になります。(aximp.exe を使って作ったものとは中身も少々違うようです)

SHDocVw.dll と AxSHDocVw.dll は何が違うかというと、AxSHDocVw.dll のソースを見ての想像ですが、前者が ActiveX コントロールのプロパティ、メソッドの COM ラッパー、後者がそれらの COM ラッパーと .NET アプリを仲介するためのプロパティ、メソッド、イベントを提供するクラスのようです。

例えば、AxSHDocVw.dll に Application というプロパティが以下のように定義されています。SHDocVw.dll には return this.ocx.Application; で使われている Application という COM ラッパーが定義されてるということのようです。

namespace AxSHDocVw {
  [System.Windows.Forms.AxHost.ClsidAttribute(
    "{8856f961-340a-11d0-a96b-00c04fd705a2}")]
  [System.ComponentModel.DesignTimeVisibleAttribute(true)]
  [System.ComponentModel.DefaultProperty("Name")]
  public class AxWebBrowser : System.Windows.Forms.AxHost {
        
    private SHDocVw.IWebBrowser2 ocx;
        
    private AxWebBrowserEventMulticaster eventMulticaster;
        
    private System.Windows.Forms.
              AxHost.ConnectionPointCookie cookie;
        
    public AxWebBrowser() : 
            base("8856f961-340a-11d0-a96b-00c04fd705a2") {
    }
    
    [System.ComponentModel.DesignerSerializationVisibility(
      System.ComponentModel.
      DesignerSerializationVisibility.Hidden)]
    [System.Runtime.InteropServices.DispIdAttribute(200)]
    public virtual object Application {
      get {
        if ((this.ocx == null)) {
          throw 
            new System.Windows.Forms.AxHost.
                InvalidActiveXStateException(
                  "Application", 
                  System.Windows.Forms.AxHost.
                    ActiveXInvokeKind.PropertyGet);
        }
        return this.ocx.Application;
      }
    }
    // ・・・中略・・・
  }
  // ・・・中略・・・
}

上記はプロパティの場合ですが、イベントの場合はもっと複雑で、AxSHDocVw.dll には、イベントの宣言、デリゲートの定義、イベントの引数クラスの定義、クライアント・シンクのクラス定義等々が含まれます。詳しくは別の記事 WebBrowser の拡張 に書きましたのでそちらを見てください。

Tags:

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar