Asp.Netエンタープライズソリューションパターン その3 フロントコントローラ

その3。
こんどはフロントコントローラ。
ASP.NETは全力でページコントローラを支援している感じなので、あえてフロントコントローラにするメリットは薄そうな気がする。
大規模なサイトなんか作ったことないからそう思うのかなぁ。



18:47
さて、3回目。ピッチを上げなくては。
とりあえず本を読むか。

18:49
ページコントローラではなく、フロントコントローラにしたくなる動機。
●ページコントローラのベースクラスが複雑化してきた。
ロジックの複雑化。
継承関係が深くなる。

*リクエストを検証し、パラメタによってぺージを遷移させるようなことはページコントローラの基本クラスでするべきではない。
すべてのページに共通の処理ではないから。ページコントローラの基本クラスでは全ページでの共通機能を書く。

●アクションやナビゲーションを外部ファイルで構成
ページコントローラではリテラルで埋めることになりがち??

*すべてのページに特定のアクションを適用するのが困難って書いてあるけど、これはページコントローラでも解決できるやろ。

URLとコントローラ。。。メリットわからん。


実装概要
ハンドラ:リクエストからパラメタを取得

コマンド:パラメタに対応したコマンドの実行。コマンド実装後、適切なビューに要求を転送

ビュー:HTMLの構築


19:09
.Netでの実装部分に入った。

19:24
実装内容をざっと読んだ。
なるほど。実行するアクションはcookieかQueryString、formかどっかに入ってて、その後の遷移先のページはUrlからマッピングするという方針らしい。
処理後に移動するページはコマンド内で決めた方がいいんじゃないか?
コマンドのアクションを実行してみないと移動先がわかんないことがありそう。
たとえば注文のエントリの内容によって、クレジット番号いれさすページに行くか、それをスキップするか、とか。。。
まぁ、遷移先のページをUrlからMapするのはRedirectingCommandの挙動だから、ヤなら違うのつくればいいということ。

19:32
本で使ってるIHttpHandlerは同期。
IHttpAsyncHandlerもあって、こっちは非同期。
IHttpAsyncHandlerを調べる。

19:50
ググったがあんまり日本語の情報が引っかからない。
あえてIHttpAsyncHandlerでやるか。

http://codezine.jp/a/article/aid/1081.aspx?p=2
http://www.ailight.jp/blog/kazuk/articles/6312.aspx

19:51
実装を始めよ。
・IHttpAsyncHandlerを使う。
・マスタページも使う。
くらいが目標でいいか。

19:58
CommandFactoryがsiteをとってきてコマンドを選んでるのはちょっと。。。変えよ。

20:01
もう少しボトムアップにクラスのソース載せる・・・わけにもいかんかぁ。

20:07
あえてXLINQも使うことにする。
XMLNodeからXElementにするにはどうすりゃいいんだ??

20:17
http://blogs.wankuma.com/pinzolo/archive/2007/04/22/72442.aspx
XDocumentとXmlDocumentの相互変換はちょっと微妙らしい。

20:22
Web.Configのどこにcontroller.mappingの記述追加すりゃいいのかわからん。
まえこれやったはずだけど、覚えてないよ。
調べた。configrationの直下でいいのか。

20:48
sectionにも書いた上でcontroller.mappingを追記しないとコンパイルエラーになるぞ。
しかしsectionに書くとタイプの指定もしないといけないな。
タイプはUrlMapでいいのか。

App_CodeにUrlMapを入れてるんだけど、コンパイル後のdllの名前がわからんぞ。
困った。

21:03
MSBuildでコンパイルしたら、PrecompiledWebにdllができるみたいなので、それでdllの名前を調べてみた。
App_Code.dllだって。そのままといえばそのまま。
とりあえずtypeにはApp_Codeを指定してみる。うまくいくのか??

21:21
XmlNodeからXElementにはこんな感じで変換した。うまくいってるっぽい。

XElement xe = XElement.Parse(section.OuterXml);
var ele = from e in xe.Descendants("entry")
select e;

21:24
ConfigurationSettings.GetConfigは古い形式だって。
じゃあ新しいのは何なんだよ。
調べる。。。System.Web.Configuration.WebConfigurationManager.GetSectionだそうだ。
もしくはSystem.Configuration.ConfigurationManager.GetSection

21:45
食事のため中断。

12:55
再開。

01:01
中断。寝る。

10:24
再開。
コリコリとコーディングを始める。
。。。VSの起動が重い。
サンプルにはないエラー発生時のページを用意したい。

10:30
IHttpAsyncHandlerを使うことにしたの忘れてた。
BeginProcessRequest内では非同期処理を開始して、すぐに制御を戻す。
EndProcessRequestで終わる。でいいのかな??
delegateを用意して非同期にしてみよう。

EndProcessRequestは処理が終わったら呼ばれるのか、中で自分で終わるのを待つのか?
どっちだ??
#処理が終わってからEndProcessRequestが呼ばれるので、自分でまたなくていいみたい。

FrontController向けにマスタページをもう一度つくらなきゃ。メンド。
コピペするか。

さて、できた。テストを開始。

10:45
web.configにHandler登録忘れてた。orz
web.configを手でいじりがちだけど、VSから変えてみる。
メニューの
webサイト→ASP.NET構成
と、思ったら、そんなに細かいところまではいじれないみたい。

既定のエラーページの構成は
アプリケーションタブでできそう。

httpHandlers 要素 (ASP.NET 設定スキーマ)
http://msdn2.microsoft.com/ja-jp/library/bya7fh0a.aspx

httpHandlers の add 要素 (ASP.NET 設定スキーマ)
http://msdn2.microsoft.com/ja-jp/library/7d6sws33.aspx

10:58
パラメタ渡す画面がいるな。

11:06
DBにデータいれないと。

11:07
マシン遅いなぁ。新しいのほしいなぁ。でもvista搭載マシンは・・・。

11:26
ページがでてくれない。。。なんで。。。

11:49
できた。
結局
httpHandlersでの指定が、

<add verb="*" path="ActualPage*" type="Handler,App_Code"/>

になってて、
Server.Transfer先のページがActualPage1.aspxになってたために、処理するhttpHanderがまた、Handlerになってしまっていたのでエラーになってたみたい。

ただ、処理中の例外はEndProcessRequestの中で自分でdelegateのEndInvokeを呼ばないと例外が発生しない。

IAsyncResult resultをSystem.Runtime.Remoting.Messaging.AsyncResultにキャストして
AsyncDelegateからDelegateをとってきて、EndInvokeを呼べば例外が発生した。

13:05
エラーページをカスタムにしよう。
調べる。
おお。三種類も方法があるのか。
http://support.microsoft.com/kb/308132/ja
http://japan.internet.com/developer/20050906/25.html

●ページのPage_Error、OnErrorで処理
Server.GetLastErrorでエラーを拾って同一ページに表示するなり、別のエラーページに飛ばすなりする

●global.asaxのApplication_Error
Server.GetLastErrorでエラーを拾ってエラーのぺージに飛ばすなりする。
エラーのロギングするならここがよさそう。

●web.configのcustomErrorsでカスタムエラーページを定義
モードに
On:カスタムエラー有効
Off:カスタムエラー無効
RemoteOnly:リモートクライアントにはカスタムエラーが表示される。デバッグに便利。
がある。

今回はPage_Errorでとって、エラーページに飛ばす。

13:26
できた。ふぅ。
ソース長いな。まぁいいか。一応載せとく。
DBアクセスはDLINQでやった。
ConfigのパースはXLINQで。あんま意味ないけど。

FcCaller.aspx(呼び出す画面)

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="FcCaller.aspx.cs" Inherits="FcCaller" %>

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>呼び出し画面</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <a href="ActualPage1?cmd=macro">macro</a>
        <br />
        <a href="FrontControllerPage1?cmd=macro">macro</a>
        <br />
        <a href="FrontControllerPage2?cmd=micro">micro</a>
    </div>
    </form>
</body>
</html>

FcCaller.aspx.cs(呼び出す画面)

using System;

public partial class FcCaller : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
    }
}

BasePageForFrontController.master(マスタページ)

<%@ Master Language="C#" AutoEventWireup="true" CodeFile="BasePageForFrontController.master.cs" Inherits="BasePageForFrontController" %>

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <asp:ContentPlaceHolder id="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body>
    <form id="headerForm" runat="server">
    <div style="background-color: #9c0001">
        <span style="font-size: small; color:#ffffff">ようこそ</span>
        <asp:Label ID="eMail" runat="server" Font-Size="Small" ForeColor="White" Text="xxx"></asp:Label>
        
    </div>
    <div style="font-size: x-large; color: #ffffff; background-color: #d3c9c7;">
        <asp:Label ID="siteName" runat="server" Text="xxx"></asp:Label>
    </div>
    <div>
        <asp:ContentPlaceHolder id="body" runat="server">
        </asp:ContentPlaceHolder>
    </div>
</form>
</body>
</html>

BasePageForFrontController.master.cs(マスタページ)

using System;

public partial class BasePageForFrontController : System.Web.UI.MasterPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (this.IsPostBack)
        {
            return;
        }

        this.eMail.Text = (string)Context.Items["address"];
        this.siteName.Text = (string)Context.Items["site"];
    }
}

ActualPage1.aspx(コンテンツ1)

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ActualPage1.aspx.cs" Inherits="ActualPage1" MasterPageFile="~/BasePageForFrontController.master" %>

<asp:Content ID="hederContent" ContentPlaceHolderID="head" runat="server">
    <title>ページ1</title>
</asp:Content>

<asp:Content ID="bodyContent" ContentPlaceHolderID="body" runat="server">
    <div style="font-size: xx-large">
    ページ<asp:Label ID="pageNumber" runat="server" Text="xxx"></asp:Label>
    </div>
</asp:Content>

ActualPage1.aspx.cs(コンテンツ1)

using System;

public partial class ActualPage1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //throw new Exception("エラーのテスト");
        this.pageNumber.Text = "1";
    }

    private void Page_Error(object sender,EventArgs e)
    {
        Context.Items["error"] = Server.GetLastError();
        Server.Transfer("FcError.aspx");
    }
}

ActualPage2.aspx(コンテンツ2)

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ActualPage2.aspx.cs" Inherits="ActualPage2" MasterPageFile="~/BasePageForFrontController.master" %>

<asp:Content ID="hederContent" ContentPlaceHolderID="head" runat="server">
    <title>ページ1</title>
</asp:Content>

<asp:Content ID="bodyContent" ContentPlaceHolderID="body" runat="server">
    <div style="font-size: xx-large">
    ページ<asp:Label ID="pageNumber" runat="server" Text="xxx"></asp:Label>
    </div>
</asp:Content>

ActualPage2.aspx.cs(コンテンツ2)

using System;

public partial class ActualPage2 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.pageNumber.Text = "2";
    }
}

FcError.aspx(エラーページ)

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="FcError.aspx.cs" Inherits="FcError" %>

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>フロントコントローラのエラーページ</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label ID="lblError" runat="server" Text="xxx"></asp:Label>
    </div>
    </form>
</body>
</html>

FcError.aspx.cs(エラーページ)

using System;

public partial class FcError : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.lblError.Text = "予期しないエラーが発生しました";
        if (!Context.Items.Contains("error"))
        {
            return;
        }

        this.lblError.Text = ((Exception)Context.Items["error"]).Message;
    }
}

Handler.cs(HttpHandler)

using System;
using System.Web;

public delegate void AsyncExecute(HttpContext context);

public class Handler : IHttpAsyncHandler
{
    public Handler()
    {
    }

    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        Command cmd = CommandFactory.Make(context.Request.Params);
        AsyncExecute ae = new AsyncExecute(cmd.Execute);
        return ae.BeginInvoke(context, cb, extraData);
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        System.Runtime.Remoting.Messaging.AsyncResult ar = result as System.Runtime.Remoting.Messaging.AsyncResult;

        if(ar==null)
        {
            return;
        }

        if(ar.EndInvokeCalled)
        {
            return;
        }

        AsyncExecute ae = ar.AsyncDelegate as AsyncExecute;

        if(ae == null)
        {
            return;
        }
        
        try
        {
            ae.EndInvoke(result);
        }catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine(e.ToString());
            throw e;
        }
    }

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        throw new NotImplementedException();
    }
}

Command.cs(Commandインターフェース)

using System.Web;

public interface Command
{
    void Execute(HttpContext context);
}


CommandFactory.cs

using System.Collections.Specialized;


public class CommandFactory
{
    private CommandFactory()
    {
    }

    public static Command Make(NameValueCollection prms)
    {
        string cmd = prms["cmd"];
        Command act = new UnknownCommand();

        if (cmd == "micro")
        {
            act = new MicroSite();
        }
        else if (cmd == "macro")
        {
            act = new MacroSite();
        }

        return act;
    }
}

RedirectingCommand.cs

using System.Web;

public abstract class RedirectingCommand
    :Command
{
    public abstract void OnExecute(HttpContext context);

    public void Execute(HttpContext context)
    {
        UrlMap map = UrlMap.Instance;
        OnExecute(context);

        string url = string.Format(
            "{0}{1}",
            map.Map[context.Request.Url.AbsolutePath],
            context.Request.Url.Query);

        context.Server.Transfer(url);
    }
}

MacroSite.cs

using System.Web;

public class MacroSite
    :RedirectingCommand
{
    public override void OnExecute(HttpContext context)
    {
        string id = context.User.Identity.Name;
        context.Items["address"] = AspnetStudyGateway.GetAddressFromCustomer(id);
        context.Items["site"] = "Macro-Site";
    } 
}

MicroSite.cs

using System.Web;

public class MicroSite
    :RedirectingCommand
{
    public override void OnExecute(HttpContext context)
    {
        string id = context.User.Identity.Name;
        context.Items["address"] = AspnetStudyGateway.GetAddressFromWebUsers(id);
        context.Items["site"] = "Micro-Site";

    } 
}

UnknownCommand.cs

using System;
using System.Web;

public class UnknownCommand
    :RedirectingCommand
{
    public override void OnExecute(HttpContext context)
    {
        throw new Exception("Urlは無効です");
    }
}

UrlMap.cs

using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web.Configuration;
using System.Xml.Linq;

public class UrlMap
    :IConfigurationSectionHandler
{
    public static UrlMap Instance
    {
        get
        {
            return (UrlMap)WebConfigurationManager.GetSection("controller.mapping");
        }
    }


    private Dictionary<string, string> map_;

    private UrlMap()
    {
    }

    public Dictionary<string, string> Map
    {
        get
        {
            return map_;
        }
    }

    public UrlMap(object parent, object configContext, System.Xml.XmlNode section)
    {
        map_ = new Dictionary<string, string>();
        XElement xe = XElement.Parse(section.OuterXml);
        var ele = from e in xe.Descendants("entry")
                  select e;

        foreach(var e in ele)
        {
            map_.Add(e.Attribute("key").Value, e.Attribute("page").Value);
        }
    }

    public object Create(object parent, object configContext, System.Xml.XmlNode section)
    {
        return new UrlMap(parent, configContext, section);
    }
}

AspnetStudyGateway.cs

using System.Linq;

public class AspnetStudyGateway
{
    public AspnetStudyGateway()
    {
    }

    public static string GetAddressFromCustomer(string id)
    {
        var u = from user in Context.customer
                where user.id == id
                select user;

        return u.First().address;
    }

    public static string GetAddressFromWebUsers(string id)
    {
        return GetAddress(id);
    }

    public static string GetAddress(string id)
    {
        var u = from user in Context.webusers
                where user.id == id
                select user;

        return u.First().address;
    }

    private static Aspnet_studyDataContext Context
    {
        get
        {
            var con = new Aspnet_studyDataContext(
                "Data Source=Gerbera;Initial Catalog=aspnet_study;User ID=sa;Password=p");
            return con;
        }
    }
}

web.configに追記したもの
configuration/configSections/に追加

        <section name="controller.mapping" type="UrlMap,App_Code"/>

configuration/に追加

    <controller.mapping>
        <entries>
            <entry key="/Work/ActualPage1" page="ActualPage1.aspx" />
            <entry key="/Work/FrontControllerPage1" page="ActualPage1.aspx" />
            <entry key="/Work/FrontControllerPage2" page="ActualPage2.aspx" />
        </entries>
    </controller.mapping>

configuration/system.web/httpHandlersに追加

            <add verb="*" path="FrontControllerPage*" type="Handler,App_Code"/>