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クラスを返す。認証済みで、それぞれのプロパティ値に値が設定されている。
・未認証の場合:GenericIdentityクラスを返す。未認証で、認証方法やユーザー名称などは空文字列。
・認証済の場合:System.Web.Security.FormsIdentityクラスを返す。認証済みで、それぞれのプロパティ値に値が設定されている。
ユーザー管理
ASP.NET MVCプロジェクト作成すると、ユーザー管理関係の環境が自動的に生成される。
下記に概要を書いておく。詳しくはAccountModels.csを参照。
ユニットテストが行えるようにこのインターフェイスで抽象化しているらしい。
内部にMembershipProvider継承クラスを持つ。ユーザー管理処理はこのプロバイダーに委譲する形になる。
どのプロバイダークラスを使用するかはWeb.configに記述する。
-> configuration/system.web/membership ノード
設定値がMemberShipクラスに登録される。内部使用のメンバーシップクラスはここから取得する。
デフォルトでは、System.Web.Security.SqlMembershipProviderクラスが指定されている。
下記に概要を書いておく。詳しくはAccountModels.csを参照。
IMembershipServiceインターフェイス
自動的に生成される、ユーザー管理のインターフェイスクラス。ユニットテストが行えるようにこのインターフェイスで抽象化しているらしい。
AccountMembershipServiceクラス
自動的に生成される、IMembersipService実装クラス。内部にMembershipProvider継承クラスを持つ。ユーザー管理処理はこのプロバイダーに委譲する形になる。
どのプロバイダークラスを使用するかはWeb.configに記述する。
-> configuration/system.web/membership ノード
設定値がMemberShipクラスに登録される。内部使用のメンバーシップクラスはここから取得する。
デフォルトでは、System.Web.Security.SqlMembershipProviderクラスが指定されている。
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ではなぜかうまく動いてくれなかったので、素直にセレクタでチェックボックスを選択している。
サブミットボタンでも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で読み込み行う。
こんなアクションメソッドを作成し、使用する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>
ルーティングのテスト
Mockクラスを使用してルーティングの単体テストを行う。
モックオブジェクトの作成
通常の実装とは異なり、Aメソッド呼び出しのときにBパラメータを渡された場合はCを返す。のような感じで動きを登録していく。
(必要な動きだけをMockクラスメソッドを使用して登録していく感じ。)
(必要な動きだけを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派生クラス
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するのを忘れないように。
※検索ページのようなパラメーター付きのテストで、コントローラー名にパラメーターも含まれてしまう。
これはどうにか対処してテストできるようにしないと、、、、
テスト対象の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メモ
ハイパフォーマンス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
(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を有効にしてコーディングの効率化を上げたり、デバッグ用クラスを定義したりする。
これを使用することで自作ライブラリのロードディング制御することができる。
スクリプトの依存関係解決、デバッグ・リリースでのロード対象切り替え機能を提供している。
デバッグバージョンスクリプトをロードするには、ロード処理前にSys.debugプロパティにtrueをセットする。
デバッグバージョンではIntelliSenseを有効にしてコーディングの効率化を上げたり、デバッグ用クラスを定義したりする。
ハイパフォーマンス Web アプリケーションの構築 - b.カスタムスクリプトの読み込み
ハイパフォーマンス Web アプリケーションの構築 - d. デバッグ時の Script Loader の使用
JavaScriptアプリケーションのパフォーマンスツール
各種ツールの紹介
DonwloadTimeOptimaizer(Doloto) Webページをプロファイリングしてスクリプトの読み込みタイミングを最適化する。 ローカルのブラウザで使用するツール?
Microsoft Ajax Minifier スクリプトファイルを小型化するツール
Internet Explorer JavaScript Profiler IEのプロファイラー
DonwloadTimeOptimaizer(Doloto) Webページをプロファイリングしてスクリプトの読み込みタイミングを最適化する。 ローカルのブラウザで使用するツール?
Microsoft Ajax Minifier スクリプトファイルを小型化するツール
Internet Explorer JavaScript Profiler IEのプロファイラー