DBを使用しないデータの永続化

HttpApplicationとHttpModuleの勉強がてら作ってみました。
こういうのは手段としてはありなんでしょうか?


下記の動作を行います。
  • http://localhost/Hogege/Group.add でグループ情報を追加、http://localhost/Hogege/Group.xml でグループ情報を表示します。
  • アプリケーションがDiposeされるときにローカルのxmlファイルにグループ情報を書き込みます。
  • また、ログイン後最初のリクエストでxmlファイルからグループ情報を読み込みします。
    //HtmlApplication
    public class MvcApplication : System.Web.HttpApplication
    {
      public Dictionary<string, List<Group>> Groups = new Dictionary<string,List<Group>>();
    
      public static void RegisterRoutes(RouteCollection routes)
      {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapRoute(
          "Hogege"
          , "Hogege/{action}.{extention}"
          , new { controller = "Hogege", extention = "html" }
        );
      
        routes.MapRoute(
          "Default", // Route name
          "{controller}/{action}/{id}", // URL with parameters
          new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );
      }
    
      protected void Application_Start()
      {
        AreaRegistration.RegisterAllAreas();
    
        RegisterRoutes(RouteTable.Routes);
      }
    }
    
    //データのロード、セーブを行うHttpModule
    public class MyHttpModule : IHttpModule
    {
      public MyHttpModule()
      {
      }
    
      public void Init(HttpApplication context)
      {
        context.Disposed += new EventHandler(context_Disposed);
        context.PostAuthorizeRequest += new EventHandler(context_PostAuthorizeRequest);
      }
    
      void context_PostAuthorizeRequest(object sender, EventArgs e)
      {
        var app = (MvcApplication)sender;
    
        if (!app.Request.IsAuthenticated)
          return;
    
        //ログインユーザーのデータをロード
        if (!app.Groups.ContainsKey(app.User.Identity.Name))
        {
          var Groups = new List<Group>();
          var grpEles = Read(app.User.Identity.Name, "Groups");
          if(grpEles != null)
          {
            foreach (var grpEle in grpEles.Elements())
              Groups.Add(Group.CreateInstance(grpEle));
            app.Groups.Add(app.User.Identity.Name, Groups);
          }
        }
      }
    
      void context_Disposed(object sender, EventArgs e)
      {
        var app = (MvcApplication)sender;
    
        try
        {
          foreach (var usr in app.Groups.Keys)
            Write(usr, "Groups", app.Groups[usr].Serialize());
        }
        catch (Exception exc)
        {
          exc.ToString();
        }
      }
      
      private XElement Read(string usr, string type)
      {
        StreamReader sr = null;
        XElement result = null;
    
        try
        {
          var path = string.Format(@"c:\work\{0}_{1}.xml", usr, type);
          if(File.Exist(path)
          {
            sr = new StreamReader(new FileStream(path
                                                , FileMode.Open
                                                , FileAccess.Read
                                                , FileShare.Read)
                                  , Encoding.UTF8);
            var xdoc = XDocument.Load(sr);
            result = xdoc.Root;
          }
        }
        finally
        {
          if (sr != null)
            sr.Close();
        }
    
        return result;
      }
    
      private void Write(string usr,string type, XElement data)
      {
        StreamWriter sw = null;
    
        try
        {
          var path = string.Format(@"c:\work\{0}_{1}.xml", usr, type);
          sw = new StreamWriter(new FileStream(path
                                      , FileMode.OpenOrCreate
                                      , FileAccess.Write
                                      , FileShare.Read)
                                , Encoding.UTF8);
    
          var xdoc = new XDocument(data);
          xdoc.Declaration = new XDeclaration("1.0", "utf-8", "yes");
          xdoc.Save(sw);
        }
        finally
        {
          if(sw != null)
            sw.Close();
        }
      }
    }
    
    //使用するデータクラス
    public class Group
    {
    	public string UserId {get;set;}
    	public string GroupId {get;set;}
    	public string GroupName {get;set;}
    	
        public XElement Serialize()
        {
            XDocument xdoc = new XDocument(new XElement("Group"
                   , new XElement("UserId", new XAttribute("value", UserId))
                   , new XElement("GroupId", new XAttribute("value", GroupId))
                   , new XElement("GroupName", new XAttribute("value", GroupName))
                                             ));
    
            return xdoc.Root;
        }
    
        static public Group CreateInstance(XElement element)
        {
            var obj = new Group();
    
            obj.UserId = element.Element("UserId").Attribute("value").Value;
            obj.GroupId = element.Element("GroupId").Attribute("value").Value;
            obj.GroupName = element.Element("GroupName").Attribute("value").Value;
    
            return obj;
        }
    }
    
    //拡張メソッド
    static public class GroupExtentions
    {
        static public XElement Serialize(this IEnumerable<Group> grps)
        {
            var ele = new XElement("Groups");
            foreach (var grp in grps)
                ele.Add(grp.Serialize());
            return ele;
        }
    }
    
    //コントローラー
    public class HogegeController : Controller
    {
        public ActionResult Index()
        {
            return new EmptyResult();
        }
    
        [HttpGet]
        public ActionResult Group(string extention)
        {
            if (Request.IsAuthenticated)
            {
    	        var groups = ((MvcApplication)HttpContext.ApplicationInstance).Groups;
    
    	        if (extention == "xml")
    	        {
    	            return new ContentResult() { 
    	                               ContentType = "Application/xml"
    	                               , Content = groups[User.Identity.Name].Serialize().ToString()
    	                       };
    	        }
    	        else if (extention == "add")
    	        {
    	            if(!groups.ContainsKey(User.Identity.Name))
    	                groups.Add(User.Identity.Name, new List<Group>());
    
    	            var newGrp = new Group(){GroupId = DateTime.Now.Ticks.ToString()
    	                                        ,GroupName = "グループ_" + DateTime.Now.Ticks.ToString()
    	                                        ,UserId = User.Identity.Name
    	                                        };
    	            groups[User.Identity.Name].Add(newGrp);
    
    	            return new ContentResult() { Content = "Add ok" };
    	        }
            }
            
            return new EmptyResult();
        }
    }
    

    ユーザー情報の取得メモ

    ユーザー情報

    System.Web.Mvc.Contoller.User.Identityで取得。
    ・未認証の場合:GenericIdentityクラスを返す。未認証で、認証方法やユーザー名称などは空文字列。
    ・認証済の場合:System.Web.Security.FormsIdentityクラスを返す。認証済みで、それぞれのプロパティ値に値が設定されている。

    ユーザー管理

    ASP.NET MVCプロジェクト作成すると、ユーザー管理関係の環境が自動的に生成される。
    下記に概要を書いておく。詳しくはAccountModels.csを参照。

    IMembershipServiceインターフェイス

    自動的に生成される、ユーザー管理のインターフェイスクラス。
    ユニットテストが行えるようにこのインターフェイスで抽象化しているらしい。

    AccountMembershipServiceクラス

    自動的に生成される、IMembersipService実装クラス。
    内部にMembershipProvider継承クラスを持つ。ユーザー管理処理はこのプロバイダーに委譲する形になる。
    どのプロバイダークラスを使用するかはWeb.configに記述する。
    -> configuration/system.web/membership ノード
    設定値がMemberShipクラスに登録される。内部使用のメンバーシップクラスはここから取得する。
    デフォルトでは、System.Web.Security.SqlMembershipProviderクラスが指定されている。

    jQueryでのチェックボックスのチェック判定

    下のような左端列にチェックボックスを持つテーブルに、選択行データの一括削除機能をjQueryで実装する。
    テーブルと削除ボタンは、↓こんな感じで。
    ...
    <table>
      <thead>
        <th> </th>
        <th>Id</th>
        <th>タイトル</th>
      </thead>
      <tbody>
        <tr class="detail-row">
          <td class="del-col"><input type="checkbox" class="del-check" /></td>
          <td class="id-col">1000</td>
          <td>タイトル1</td>
        </tr>
        <tr class="detail-row">
          <td class="del-col"><input type="checkbox" class="del-check" /></td>
          <td class="id-col">1001</td>
          <td>タイトル2</td>
        </tr>
        ...
      </tbody>
    </table>
    ...
    <input type="submit" id="del-button" value="削除" />
    

    削除ボタン押下時処理
    サブミットボタンでもclickイベントをバインドする。
    falseを返すようにすれば処理を中断することができる。(ただしその場合はバブリングは発生せずにここでとまる。)
    チェックボックスのチェック状態は、checked属性値(bool型)で判定する。 jQueryの:checkedや:checkboxではなぜかうまく動いてくれなかったので、素直にセレクタチェックボックスを選択している。
    $("#del-button").bind("click", function() {
      
      if (!window.confirm("削除しますか?"))
        return false;
    
      var ids = "";
      $(".del-check").each(function() {
        if ($(this).attr("checked")) {
            var r_idx = $("tr").index($(this).parent().parent().get(0));
            if (ids != "")
                ids += ",";
            ids += $("tr:eq(" + r_idx + ") .id-col").text();
        }
      });
    
      $("#Id").attr("value", ids);
    
      if (ids == "") {
        window.alert("削除対象が選択されていません。");
        return false;
      }
    
      return true;
    });
    

    JavaScriptからアクションメソッド呼び出し

    Tableのtrクリックイベントで、アクションメソッド呼び出しを行うといったことを実現するために
    こんなアクションメソッドを作成し、使用するViewで読み込み行う。
    public ActionResult HogeeUrl()
    {
      return JavaScript(String.Format("var hogeUrl = {0};", Url.Content("~/Foo/Hogee"));
    }
    

    <html>
      <head>
        <script type="text/javascript" src="<%= Url.Content("~/Foo/HogeeUrl") %>"></script>
        <script type="text/javascript">
          //ここでアクションメソッド呼び出しを行う。hogeUrl変数値を使用する。
        </script>
      </head>
      <body>
      ....
      </body>
    </html>
    


    このように回りくどいことをしていたが、素直にViewDataにアクションメソッドのUrlを入れて、
    ViewにHiddenで埋め込み、jQueryセレクタ使用して取得したほうが手っ取り早いことに気づいた。。。

    ルーティングのテスト

    Mockクラスを使用してルーティングの単体テストを行う。

    モックライブラリ

    http://code.google.com/p/moq/

    使い方は、配布ページドキュメントと、ここを参考にした。

    モックオブジェクトの作成
    通常の実装とは異なり、Aメソッド呼び出しのときにBパラメータを渡された場合はCを返す。のような感じで動きを登録していく。
    (必要な動きだけをMockクラスメソッドを使用して登録していく感じ。)
    //こんな感じで
    
    interface IEvenOdd
    {
        //偶数か
        bool IsEven(int val);
        //奇数か
        bool IsOdd(int val);
    }
    
    static class EvenOddFactory
    {
        static IEvenOdd Create()
        {
            var mock = new Mock<IEvenOdd>();
    
            //テストでは1から5までの値だけしか使用されないので、
            //必要な動きだけ定義する。
    
            //偶数
            mock.Setup(eo => eo.IsEven(2)).Returns(true);
            mock.Setup(eo => eo.IsEven(4)).Returns(true);
            //奇数
            mock.Setup(eo => eo.IsOdd(1)).Returns(true);
            mock.Setup(eo => eo.IsOdd(3)).Returns(true);
            mock.Setup(eo => eo.IsOdd(5)).Returns(true);
    
            return mock.Object;
        }
    }
    

    テスト用のHttpContextBase派生クラス

    モックライブラリを使ってHttpContextBase派生クラスを生成する。
    ここを参考に作成。
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    using Moq;
    
    namespace SimpleBBS2
    {
    	public static class MockHttpContextFactory
    	{
    	    public static HttpContextBase Create(string url)
    	    {
    	        var hcb_mock = new Mock<HttpContextBase>();
    	        var req_mock = new Mock<HttpRequestBase>();
    
    	        req_mock.Setup(req => req.AppRelativeCurrentExecutionFilePath).Returns("~/" + url);
    	        hcb_mock.Setup(hcb => hcb.Request).Returns(req_mock.Object);
    
    	        return hcb_mock.Object;
    	    }
    	}
    }
    

    ルーティングテスト

    こんな感じ。
    テスト対象のHttpApplication派生クラスを使ってRegisterRoutesするのを忘れないように。

    ※検索ページのようなパラメーター付きのテストで、コントローラー名にパラメーターも含まれてしまう。
    これはどうにか対処してテストできるようにしないと、、、、
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    using NUnit.Framework;
    using Moq;
    
    
    namespace SimpleBBS2Test
    {
        [TestFixture]
        public class RouteTest
        {
            private abstract class PageTest
            {
                protected RouteCollection rc;
    
                protected PageTest(RouteCollection rc)
                {
                    this.rc = rc;
                }
    
                public abstract void Test();
            }
    
    
            private class CreateTest : PageTest
            {
                public CreateTest(RouteCollection rc) : base(rc)
                {
                }
    
                public override void Test()
                {
                    {
                        var hc = MockHttpContextFactory.Create("SimpleBBS2/Thread/Create");
                        var rd = rc.GetRouteData(hc);
                        Assert.AreEqual("SimpleBBS2", rd.Values["controller"]);
                        Assert.AreEqual("CreateThread", rd.Values["action"]);
                    }
    
                    {
                        var hc = MockHttpContextFactory.Create("SimpleBBS2/CreateThread");
                        var rd = rc.GetRouteData(hc);
                        Assert.AreEqual("SimpleBBS2", rd.Values["controller"]);
                        Assert.AreEqual("CreateThread", rd.Values["action"]);
                    }
                }
            }
    
            private class ThreadListTest : PageTest
            {
                public ThreadListTest(RouteCollection rc)
                    : base(rc)
                {
                }
    
                public override void Test()
                {
                    {
                        var hc = MockHttpContextFactory.Create("SimpleBBS2/ThreadList");
                        var rd = rc.GetRouteData(hc);
                        Assert.AreEqual("SimpleBBS2", rd.Values["controller"]);
                        Assert.AreEqual("ThreadList", rd.Values["action"]);
                    }
                }
            }
            
            private class SearchTest : PageTest
            {
                public SearchTest(RouteCollection rc)
                    : base(rc)
                {
                }
    
                public override void Test()
                {
                    {
                        var hc = MockHttpContextFactory.Create("SimpleBBS2/Search");
                        var rd = rc.GetRouteData(hc);
                        Assert.AreEqual("SimpleBBS2", rd.Values["controller"]);
                        Assert.AreEqual("Search", rd.Values["action"]);
                        Assert.AreEqual(null, rd.Values["t"]);
                        Assert.AreEqual(null, rd.Values["p"]);
                    }
    
                    //↓クエリ文字列付きだと、アクション名にクエリ文字列が含まれてしまう。
                    
                    //{
                    //    var hc = MockHttpContextFactory.Create("SimpleBBS2/Search?t=タグ");
                    //    var rd = rc.GetRouteData(hc);
                    //    Assert.AreEqual("SimpleBBS2", rd.Values["controller"]);
                    //    Assert.AreEqual("Search", rd.Values["action"]);
                    //    Assert.AreEqual("タグ", rd.Values["t"]);
                    //    Assert.AreEqual(null, rd.Values["pageno"]);
                    //}
                    //
                    //{
                    //    var hc = MockHttpContextFactory.Create("SimpleBBS2/Search?t=タグ&pageno=1");
                    //    var rd = rc.GetRouteData(hc);
                    //    Assert.AreEqual("SimpleBBS2", rd.Values["controller"]);
                    //    Assert.AreEqual("Search", rd.Values["action"]);
                    //    Assert.AreEqual("タグ", rd.Values["t"]);
                    //    Assert.AreEqual("1", rd.Values["pageno"]);
                    //}
                }
            }
    
            [Test]
            public void DoTest()
            {
                var rc = new RouteCollection();
                SimpleBBS2.MvcApplication.RegisterRoutes(rc);
    
                var tests = new List<PageTest>();
                tests.Add(new CreateTest(rc));
                tests.Add(new ThreadListTest(rc));
                tests.Add(new SearchTest(rc));
    
                foreach (var test in tests)
                    test.Test();
            }
        }
    }
    

    IISメモ

    環境:Windows7Pro64bit IIS7.5
    自分用メモ。俺語が出てくる場合があります。

    管理画面

    1つのISSで複数Webサイトを管理することができる。
    Webサイトでは仮想ディレクトリ、Webアプリケーションを複数管理することができる。

    Webサイトのバインド

    Webサイトは設定されたバインドルールに従って(IPアドレス、ドメイン名、ポート番号、プロトコル)
    バインディングされる。

    Webサイトへのアイテム追加

    Webサイトツリーアイテムのコンテキストメニューから"仮想ディレクトリの追加"、"アプリケーションの追加"を行う。

    アイテム追加をする前に

    仮想ディレクトリ、WebアプリケーションはIUSER権限で実行されるので
    十分な権限があることを確認する。

    仮想ディレクトリの追加

    静的なWebページなどを追加する。
    バインドする仮想パスと物理パスを設定する。

    アプリケーションの追加

    ASP.NETアプリケーションを追加する。
    バインドする仮想パスと物理パス、アプリケーションプールを設定する。

    ※アプリケーションプール
    IISのワーカープロセスで、通常はWebサイト1つにつき1アプリケーションプールが割り当てられている。
    これは設定により変更可能

    ハイパフォーマンスWebアプリケーション

    http://msdn.microsoft.com/ja-jp/asp.net/ff394368.aspx のまとめ

    Microsoft Ajaxコンテンツ配信ネットワーク(CDN)

    世界中の戦略的なネットワークポイントに配置されているコンテンツ配信サーバー
    (MS社製のjavascriptファイル、jqueryファイルを配信する)のネットーワーク。
    アクセス元からもっとも近いコンテンツ配信サーバーから提供を受ける。
    さらに、クライアントブラウザーのキャッシュに受信コンテンツを格納して
    再利用を行うようになている。
    -> ブラウザからスクリプトUrlにアクセスすると、cacheファイルのダウンロードダイアログが出る。

    jQueryの場合だと
    http://ajax.microsoft.com/ajax/jquery/jquery-1.3.2.min.js


    並列に動的にスクリプトをロードする(ASP.NET Ajax Script Loader)

    Start.jsというScriptLoaderを使用する。ただし、Sys.scriptsコレクションで定義しているものだけ指定することができる。
    明示的にrequireメソッドを呼び出す必要がある。これを利用して、例えば、ボタン押下イベント内でrequire呼び出しを行うようにして
    遅延ローディングを実現することができる。
    また、スクリプトの依存関係に対応するために、require呼び出しを行うと指定スクリプトとそれが依存しているスクリプトの両方がロードされる。
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/beta/0911/Start.js"></script>
    <script type="text/javascript">
    //Sys.requireでロード対象スクリプトライブラリの配列を渡す。
    //非同期なのですぐに次処理に移る。
    Sys.require([Sys.components.dataView, Sys.scripts.jQuery]);
    window.alert("ロード中です。");
    
    //スクリプトロード時処理
    Sys.onReady(function(){
      window.alert("ロード完了");
    });
    </script>
    


    カスタムスクリプトのロード

    カスタムスクリプトをロードするには、Sys.loader.defineScriptsを使用する。
    これを使用することで自作ライブラリのロードディング制御することができる。
    スクリプトの依存関係解決、デバッグ・リリースでのロード対象切り替え機能を提供している。

    デバッグバージョンスクリプトをロードするには、ロード処理前にSys.debugプロパティにtrueをセットする。
    デバッグバージョンではIntelliSenseを有効にしてコーディングの効率化を上げたり、デバッグ用クラスを定義したりする。

    ハイパフォーマンス Web アプリケーションの構築 - b.カスタムスクリプトの読み込み
    ハイパフォーマンス Web アプリケーションの構築 - d. デバッグ時の Script Loader の使用



    JavaScriptアプリケーションのパフォーマンスツール

    各種ツールの紹介

    DonwloadTimeOptimaizer(Doloto) Webページをプロファイリングしてスクリプトの読み込みタイミングを最適化する。 ローカルのブラウザで使用するツール?

    Microsoft Ajax Minifier スクリプトファイルを小型化するツール

    Internet Explorer JavaScript Profiler IEのプロファイラー