WebSurfer's Home

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

InnerException

by WebSurfer 2012年11月10日 10:14

例外の種類によって処置を分けるような場合、InnerException プロパティの情報が必要な場合があります。

例えば、先の記事 更新操作中の例外の処 置 のサンプルを見てください。この例では、DetailsViewInsertedEventArgs オブジェクトから取得できる Exception オブジェクトの InnerException に格納されているオリジナルの例外を調べて処置を分けています。

ところが、SqlDataSource と GridView を組み合わせて更新操作を行うような場合、GridViewUpdatedEventArgs オブジェクトから取得できる Exception オブジェクトには InnerException は含まれません。Exception オブジェクトそのものがオリジナルの例外となり、InnerException プロパ ティは null を返します。

どのような場合に InnerException が含まれるのか/含まれないのか、自分が探した限りですが、 MSDN ライブラリなどには明確な記述は見つけら れませんでした。

なので InnerException を見るべきか Exception そのものを見るべきかは、コードを書いて実際に動かしてみて調べないとわかないということにな ります。

さらに、InnerException が何重かネストしているケースがあるかもしれません。

というわけで、再帰を使って一番最初に発生した例外を取得するのがよさそうです。以下のような感じです。

public static Exception GetInnerException(Exception ex)
{
  if (ex.InnerException == null)
  {
    return ex;
  }
  return GetInnerException(ex.InnerException);
}
    
protected void GridView1_RowUpdated(object sender, 
  GridViewUpdatedEventArgs e)
{
  if (e.Exception == null && e.AffectedRows == 1)
  {
    // Update に成功
  }
  else
  {
    if (e.Exception != null)
    {
      Exception ex = GetInnerException(e.Exception);
            
      if (ex is SqlException)
      {
        if (((SqlException)ex).Number == 2627)
        {
          // PK 制約違反
          Label1.Text = ex.Message;
          e.ExceptionHandled = true;
        }
      }
    }
    e.KeepInEditMode = true;
  }
}

実際はここまでやる必要はなく、せいぜい一つ下まで調べて、コードを書けば十分かもしれません。

Tags:

Exception Handling

更新操作中の例外の処置

by WebSurfer 2011年8月16日 17:25

ユーザー入力を使ってデータベースの更新を行うケースで、ユーザー入力の検証結果が NG の場合、先にデータバインドコントロール(ここでは DetailsView を例にします)にユーザーが入力したデータを維持したままユーザーに再入力を促すにはどうすればよいかという話です。

ユーザーが入力したデータを維持したままユーザーに再入力を促す

ASP.NET には、ユーザー入力を検証するために RegularExpressionValidator, CustomValidator 等の検証のためのコントロールが用意されており、通常はこれらを組み合わせて使えば簡単に要件は満たせます。

ただし、検証するタイミングが更新の直前でなければならず、かつ DB ベースから値を取得して検証し、結果 NG ならロールバックしなければならないような場合は、上記の検証コントロールを使って目的を果たすのは難しいです。

その場合は、ObjectDataSource と型付 DataSet + TableAdapter を DetailsView と組み合わせた 3 層構造とし、検証やロールバック等の処理は TableAdapter を拡張したメソッドを作成して行うのが良いと思いますが、検証結果が NG の時 DetailsView にユーザーが入力したデータを維持するには工夫が必要です。

例えば、拡張したメソッドの戻り値などで検証結果 NG を判断して、DetailsView を挿入モードに保ってユーザーに再入力を促すことはできます。しかしながら、その前に DetailsView に DataBind が起こって、せっかくユーザーが入力したデータがすべて消えてしまいます。

それを避けるためには、拡張したメソッドで検証結果 NG と判定された場合は、そこで例外をスローすることです。ただし、スローしっぱなしではサーバーエラーで終わってしまうので、スローされた例外を処置しなければなりません。

処置と言っても、拡張したメソッド内で try - catch で行うのではありません。拡張したメソッドは、検証結果 NG の場合に特定の例外をスローするだけです。

処置は ItemInserted イベントのイベントハンドラに渡された DetailsViewInsertedEventArgs オブジェクトを使用して行います。

検証結果 NG でスローされた特定の例外を捕捉した場合のみ ExceptionHandled プロパティを true に設定することで例外は処置されたと見なされます。

以下に検証に使用した aspx ページのコードをアップしておきます。ArgumentNullException と NorthwindDataException のみを捕捉し、ExceptionHandled プロパティを true に設定しています。

<%@ Page Language="C#" %>
<%@ Import Namespace="ProductsDataSetTableAdapters" %>

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

<script runat="server">
  protected void Page_Load(object sender, EventArgs e)
  {
    MessageLabel.Text = String.Empty;
  }
    
  protected void DetailsView1_ItemInserted(object sender, 
    DetailsViewInsertedEventArgs e)
  {
    if (e.Exception == null && e.AffectedRows == 1)
    {
      MessageLabel.Text = "Insert に成功";
    }
    else
    {
      if (e.Exception != null)
      {
        Exception innerEx = e.Exception.InnerException;
        if (innerEx is ArgumentNullException ||
          innerEx is ProductsTableAdapter.NorthwindDataException)
        {
          MessageLabel.Text = innerEx.Message;
          e.ExceptionHandled = true;
        }
      }
      else
      {
        MessageLabel.Text = "例外の発生なし。";
      }            
      e.KeepInInsertMode = true;
    }
  }
    
  protected void DetailsView1_ItemUpdated(object sender, 
    DetailsViewUpdatedEventArgs e)
  {
    if (e.Exception == null && e.AffectedRows == 1)
    {
      MessageLabel.Text = "Update に成功。";
    }
    else
    {
      if (e.Exception != null)
      {
        Exception innerEx = e.Exception.InnerException;
        if (innerEx is ArgumentNullException ||
          innerEx is ProductsTableAdapter.NorthwindDataException)
        {
          MessageLabel.Text = innerEx.Message;
          e.ExceptionHandled = true;
        }
      }
      else
      {
        MessageLabel.Text = "例外の発生なし。";
      }       
      e.KeepInEditMode = true;
    }
  }

  protected void DetailsView1_DataBinding(object sender, 
    EventArgs e)
  {
    // 例外が出ない場合は DataBind がかかる。
    // 結果、ユーザー入力が消える。
    MessageLabel.Text += " DataBinding イベントが発生";
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <h2>挿入、更新操作中の例外の処置</h2>
    <asp:ObjectDataSource ID="ObjectDataSource1" 
      runat="server"
      SelectMethod="GetData" 
      DeleteMethod="Delete" 
      InsertMethod="ThrowExceptionBeforeInsert" 
      UpdateMethod="ThrowExceptionBeforeUpdate"
      OldValuesParameterFormatString="original_{0}" 
      TypeName="ProductsDataSetTableAdapters.ProductsTableAdapter">
      <DeleteParameters>
        <asp:Parameter Name="Original_ProductID" Type="Int32" />
      </DeleteParameters>
      <InsertParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="SupplierID" Type="Int32" />
        <asp:Parameter Name="CategoryID" Type="Int32" />
        <asp:Parameter Name="QuantityPerUnit" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="UnitsInStock" Type="Int16" />
        <asp:Parameter Name="UnitsOnOrder" Type="Int16" />
        <asp:Parameter Name="ReorderLevel" Type="Int16" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
      </InsertParameters>
      <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="SupplierID" Type="Int32" />
        <asp:Parameter Name="CategoryID" Type="Int32" />
        <asp:Parameter Name="QuantityPerUnit" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="UnitsInStock" Type="Int16" />
        <asp:Parameter Name="UnitsOnOrder" Type="Int16" />
        <asp:Parameter Name="ReorderLevel" Type="Int16" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="Original_ProductID" Type="Int32" />
      </UpdateParameters>
    </asp:ObjectDataSource>
    <asp:DetailsView ID="DetailsView1" 
      runat="server" 
      AllowPaging="True" 
      AutoGenerateRows="False" 
      DataKeyNames="ProductID" 
      DataSourceID="ObjectDataSource1" 
      OnItemInserted="DetailsView1_ItemInserted" 
      OnItemUpdated="DetailsView1_ItemUpdated"
      OnDataBinding="DetailsView1_DataBinding">
      <Fields>
        <asp:BoundField DataField="ProductID" 
          HeaderText="ProductID" 
          InsertVisible="False" 
          ReadOnly="True" 
          SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" 
          HeaderText="ProductName" 
          SortExpression="ProductName" />
        <asp:BoundField DataField="SupplierID" 
          HeaderText="SupplierID" 
          SortExpression="SupplierID" />
        <asp:BoundField DataField="CategoryID" 
          HeaderText="CategoryID" 
          SortExpression="CategoryID" />
        <asp:BoundField DataField="QuantityPerUnit" 
          HeaderText="QuantityPerUnit" 
          SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" 
          HeaderText="UnitPrice" 
          SortExpression="UnitPrice" />
        <asp:BoundField DataField="UnitsInStock" 
          HeaderText="UnitsInStock" 
          SortExpression="UnitsInStock" />
        <asp:BoundField DataField="UnitsOnOrder" 
          HeaderText="UnitsOnOrder" 
          SortExpression="UnitsOnOrder" />
        <asp:BoundField DataField="ReorderLevel" 
          HeaderText="ReorderLevel" 
          SortExpression="ReorderLevel" />
        <asp:CheckBoxField DataField="Discontinued" 
          HeaderText="Discontinued" 
          SortExpression="Discontinued" />
        <asp:CommandField ShowDeleteButton="True" 
          ShowEditButton="True" 
          ShowInsertButton="True" />
      </Fields>
    </asp:DetailsView>
    <asp:label id="MessageLabel"
      forecolor="Red"
      runat="server"/> 
  </div>
  </form>
</body>
</html>

TableAdapter を拡張したメソッドは以下の通りです。partial class として別ファイルにコードを書いて簡単に拡張できます。なお、以下のコードは検証用ですので、例外をスローするだけで、データーベースを更新するコードは含まれていません。

using System;
using System.Data;
using System.Configuration;
using System.Data.SqlClient;
using System.ComponentModel;

namespace ProductsDataSetTableAdapters
{
  public partial class ProductsTableAdapter
  {
    public virtual int ThrowExceptionBeforeInsert(
      string ProductName, 
      Nullable<int> SupplierID, 
      Nullable<int> CategoryID, 
      string QuantityPerUnit, 
      Nullable<decimal> UnitPrice, 
      Nullable<short> UnitsInStock, 
      Nullable<short> UnitsOnOrder, 
      Nullable<short> ReorderLevel, 
      bool Discontinued)
    {
      if (ProductName == null)
      {
        throw new ArgumentNullException("ProductName");
      }
      else if (ProductName == "xxx")
      {
        throw new ApplicationException("ハンドルされない例外");
      }
      else if (ProductName == "zzz")
      {
        return 0;
      }
      else
      {
        throw new NorthwindDataException("Insert で例外。");
      }
    }

    public virtual int ThrowExceptionBeforeUpdate(
      Nullable<int> Original_ProductID,
      string ProductName,
      Nullable<int> SupplierID,
      Nullable<int> CategoryID,
      string QuantityPerUnit,
      Nullable<decimal> UnitPrice,
      Nullable<short> UnitsInStock,
      Nullable<short> UnitsOnOrder,
      Nullable<short> ReorderLevel,
      bool Discontinued)
    {
      if (ProductName == null)
      {
        throw new ArgumentNullException("ProductName");
      }
      else if (ProductName == "xxx")
      {
        throw new ApplicationException("ハンドルされない例外");
      }
      else if (ProductName == "zzz")
      {
        return 0;
      }
      else
      {
        throw new NorthwindDataException("Update で例外。");
      }
    }

    public class NorthwindDataException : Exception
    {
        public NorthwindDataException(string msg) : base(msg) { }
    }

  }
}

Tags:

Exception Handling

customErrors と requestFiltering

by WebSurfer 2010年10月12日 12:17

先の記事 アプリケーションレベルの例外処理 で書きました、(1) Button 6 クリックおよび (2) 存在しない静的ファイルを要求した場合は、web.config の customErrors 要素にカスタムエラーページを設定してもそれが表示されない(下の画像のような標準エラーページが表示される)件につき理由を調べてみました。

標準エラーページ

調査結果をまとめると以下のとおりです(自信度 99% ぐらい。残り 1% の不安要素は後述します)。

(1) Button 6 クリック(Global-NoCatch.asax へリダイレクト)

IIS7 で導入された 要求のフィルタリング で、特定の拡張子(asa, asax, ascx など)のファイルに対する要求がブロックされています。

Global-NoCatch.asax へリダイレクトした場合は以下のようになります。拡張子でチェックしているので Global.asax でも結果は同じです。

要求フィルタモジュール (RequestFilteringModule) が拡張子 asax をチェックして拒否。 → 静的ファイルハンドラー (StaticFile) によって 404.7 エラーとして処理される。 → デフォルト(httpErrors 設定なし)では標準エラーページを返す。

先に applicationHost.config の設定を変えて(fileExtension=".asax" allowed="false" を true にして)試したときも、やはり標準エラーページになったのは、asax が静的ファイルハンドラー (StaticFile) によって処理されたためと思われます。

注意:
2014/4/19 時点の最新のサンプルでは、Button 6 クリックでのリダイレクト先が Global-NoCatch.asax から NonexistentPage-NoCatch.aspx/xxx...(xxx... は x が 500 文字)に変わっています。(遅くとも 2011/4/23 には変わっていたはず) 結果、HTTP/1.1 400 Bad Request が返ってきます。詳しくは下の「2011/4/23 追記」と「2011/8/22 追記」を見てください。

(2) 存在しない静的ファイルを要求

統合パイプラインモードでも既存のハンドラーマッピングはすべて機能したままなので、静的ファイルは依然として IIS のネイティブの静的ファイルハンドラー (StaticFile) によって処理される。 → デフォルト(httpErrors 設定なし)では標準エラーページを返す。

以上から、要求フィルタリングで拒否設定されているファイルや存在しない静的ファイルを要求されて、カスタムエラーページを返には、TechNet のページ HTTP エラー <httpErrors> の中の「カスタムエラーページを追加する方法」で述べられている手順を取るほかなさそうです。

なお、開発サーバーで検証すると動作が異なる件は、今後二度と開発サーバーを検証に利用しないことにするということで、気にしないことにしました。(笑) 開発サーバーには要求フィルタリングはなく、例外はすべて ASP.NET で処置されるということなのかもしれません(想像です)。

------ 2011/4/23 追記(2014/4/19 一部訂正) ------

上に書いたとおり、MSDN ライブラリの英語版 Complete Example for Error Handlers にコメントしましたが、それを受けてコードが一部変更されています。(日本語の方は 2011/4/23 現在以前のままですが)

具体的には、Button6 クリックでのリダイレクト先を Global-NoCatch.asax から NonexistentPage-NoCatch.aspx/xxx...(xxx... は x が 500 文字)に変えています。

でも、依然としてダメです。DefaultRedirectErrorPage.aspx にはリダイレクトされません。サーバーは HTTP/1.1 400 Bad Request を返します。


------ 2011/8/22 追記(2014/4/19 一部訂正) ------

MSDN ライブラリの英語版 Complete Example for Error Handlers に書いたコメントはすでに消されていました。また、「日本語の方は 2011/4/23 現在以前のまま」と書きましたが、英語版と同様に修正されていました。

ただし、コードの内容は 2011/4/23 時点の英語版のままです。Button 6 の説明に "Click this button to create an HTTP 400 (invalid url) error. Application_Error will catch this but will not take any action on it, and ASP.NET will redirect to DefaultRedirectErrorPage.aspx." と書いてありますが、そうはなりません。

NonexistentPage-NoCatch.aspx/xxx...(xxx... は x が 500 文字)を要求すると、サーバーは HTTP/1.1 400 Bad Request を返します。web.config の defaultRedirect に指定した DefaultRedirectErrorPage.aspx にはリダイレクトされません。

(HTTP/1.1 400 Bad Request が返ってくると、IE に表示されるのはサーバーから帰ってきた html コードではなく IE が差し替えたものになるので注意してください。Firefox の場合はサーバーから帰ってきた html コードがそのまま表示されます)

ちなみに、以前のように Global-NoCatch.asax にリダイレクトすると、「HTTP エラー 404.7 - Not Found 要求フィルタ モジュールが、ファイル拡張子を拒否するように構成されています。」という標準エラーページが返ってきます。

Tags: ,

Exception Handling

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar