2009年12月14日月曜日

Google Map上に線を描画する方法

今回はGoogle Map上に線を引く方法を解説する。



実際に動いているものは下記を参照して欲しい。
http://www.my-clip.net/Clip/Details/38

var lineColor = "#0000af";
var lineWeight = 3;
var lineOpacity = .8;
var points = [];
for (i = 0; i < markers.length; i++) {
var point = new GLatLng(markers[i].Latitude, markers[i].Longitude);
var marker = new GMarker(point)
marker.description = markers[i].Description;
map.addOverlay(marker);
GEvent.addListener(marker, &quot;click&quot;, function() {
openInfoWindow(this, this.description);
});
points.push(point);
}

var polyLine = new GPolyline(points, lineColor, lineWeight, lineOpacity);
map.addOverlay(polyLine);
}

上記のコードを見てもらえば分かるように地図上に線を描画するのは至極簡単だ。描画したい線の座標を順番にGLatLngの配列へと設定し、GPolylineのコンストラクタへ渡すだけで良い。

2009年12月9日水曜日

LinqToSqlでランダムセレクト

今回はLinqToSqlでレコードをランダムに取得する方法を解説する。ランダムに画像を複数件表示しなければならないときに実装した方法だ。

通常のSQLでは下記のようなクエリとなる。

select top 10 * from table order by NEWID()

これで10件のレコードをランダムに取得できる。これをLinqで記述すると下記になる。

partial class MyDataContext { 
     [Function(Name="NEWID", IsComposable=true)]  
     public Guid Random()  
     {
         throw new NotImplementedException();  
     } 
} 

var cust = (from row in db.Table
           order by db.Random() 
           select row).Take(10); 

元ネタはStack Overflowの下記のリンクだ。処理の詳細はSQL ServerのNEWID関数をRandomというDataContextの関数として実装し、前述のSQLをLinq上で再現している。

http://stackoverflow.com/questions/648196/random-row-from-linq-to-sql

上記リンク先でも指摘されているが、この方法は小さいテーブル用だ。多量のレコードに対して行うと大変な負荷をかけるので避けるようにしたい。大きいテーブル用のアプローチ方法としては全レコードに対してNEWIDするのではなく、ある程度小さくした集団に対して使用するべきだろう。上記リンク先にも大きいテーブル用のアプローチ方法が記載されいてるがここで紹介している内容と処理内容が異なるのでとくには解説しない。

ランダム、ランダムと書いていたらランダムがゲシュタルト崩壊してきた・・・。

2009年11月29日日曜日

LinqToSqlでの開発の注意点 その2

今回はLinqToSqlを使用してDBアクセスする際の注意点を解説する。

LinqToSqlで開発する場合、DBMLファイルを使用しDBMLで生成されたDataContextを使用するのが常道だろう。その際に、LinqToSqlを介しDBのテーブルにマッピングされたクラスを使用していると、ともするとメモリ上のデータを扱っているような錯覚を起こす場合がある。そのため経験の浅いプログラマはパフォーマンスを考慮せずに実装したりするのだが、実際にCo-opの学生が実装したコードを次に紹介しよう。

using( var db = new HogeDataContext())
{
 foreach( var user in db.Users.Where(x=> x.Hoge))
 {
  DoSomething(user);
  var userClients = user.UserClients.Select( x => new SimpleUserClient(){ Name=x.Name, Address=x.Address });
  DoSomethingWithClients(userClients);
 }
}

上記の例は実際に彼が実装していたのとは異なるのだが、やっているコンセプトは同じだ。もちろん上記のコードで期待した動作はできるのだがDBのパフォーマンス上よろしくない。

その理由は上記のコードをSqlに展開するとよく分かる。展開したSqlが下記になる。

select * from Users where Hoge=1
select Name, Address from UserClients where UserId=1
select Name, Address from UserClients where UserId=2
select Name, Address from UserClients where UserId=n




という風に2行目以降は1行目で取得したUser分だけ生成、実行されることになる。仮にUserを100人分取得した場合は100回DBにアクセスすることになる。SqlServer 2008とともにインストールされているSql Server Profilerを起動してから上記のコードを実行すると実際にSqlServer上で実行されているSqlが参照できるので良く分かると思う。

実際にはこのように書くべきだ。

using( var db = new HogeDataContext())
{
 foreach( var user in db.Users.Where(x=> x.Hoge).Select(x=> new { User=x, Clients=x.Clients.Select(x=>new SimpleUserClient(){ Name=x.Name, Address=x.Address }) })
 {
  DoSomething(user.User);
  DoSomethingWithClients(user.Clients);
 }
}

これでSqlの実行は一度だけになりDBのリソースを無駄に消費することはなくなった。最後に上記のコードから生成されるSqlを紹介しておこう。(実際に生成されたSqlの不要な部分を削ったりしてあります)

SELECT [t0].[UserId], [t0].[UserName], [t1].[Name], [t1].[Address](
SELECT COUNT(*)
FROM [dbo].[UserClients] AS [t2]
WHERE [t2].[UserId] = [t0].[UserId]
) AS [value]
FROM [dbo].[Users] AS [t0]
LEFT OUTER JOIN [dbo].[UserClients] AS [t1] ON [t1].[UserId] = [t0].[UserId]
ORDER BY [t0].[UserId], [t1].[Id]

Co-opの彼が担当した部分に今回紹介したようなコードが大量に散見されたために卒倒しそうになったので、今回紹介したようなコードを安易に大量生成するのはやめよう。

2009年11月18日水曜日

IModelBinderの話

今回はカスタムModelBinderを解説する。ModelBinderとはリクエストパラメータとControllerのActionの引数になっている型を見て、リクエストパラメータがその引数に変換可能であれば値を変換・設定してくれるという何とも素晴らしい機能のことだ。

ModelBinderの動作は下記のようになる。

-Model-
class Clip
{
 public string Description { get; set; }
}

-View-
<%Html.TextBox("Description")%>

-Controller-
pubilc ActionResult AddClip(Clip clip)
{
 var description = clip.Description;   // You can directly retrieve Description here!!
 // code here...
}

上記の例でModelBinderは次の処理を行っている。Controller.AddClipの引数にClipクラスがあり、そのクラスのプロパティにDescriptionがあるのでリクエストパラメータのDescriptionという名前の値をClip.Descriptionに代入している。

と、このようにModelBinderは値の変換、代入という煩雑な処理を一手に引き受けてくれる大変便利でシンプルな構造になっているのが理解できたかと思う。しかし、Viewが複雑になってくると独自にバインド処理を記述したくなる場面がでてくる。その方法をここから解説しよう。

ここではViewから配列情報を格納したJsonを文字列として受け取り、そのJsonをListクラスに変換・代入するという男気あふれる処理を行う。

-Model.cs-
[ModelBinder(typeof(PathModelBinder))]
public class PathModel
{
 public string Title { get; set; }
 public List<PathItemModel> Paths { get; set; }
 public string PathsJson { get; set; }

 public void SetModelValue(ModelStateDictionary modelState)
 {
  modelState.SetModelValue("Title", new ValueProviderResult(this.Title, this.Title, null));
  modelState.SetModelValue("PathsJson", new ValueProviderResult(this.PathsJson, this.PathsJson, null));
 }
}

public class PathModelBinder : IModelBinder
{
 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {            
  var list = JsonConvert.DeserializeObject<List<PathItemModel>>(bindingContext.ValueProvider["PathsJson"].AttemptedValue);
  var model = new PathModel()
  {
   Paths = list,
   Title = bindingContext.ValueProvider["Title"].AttemptedValue,
   Description = bindingContext.ValueProvider["Description"].AttemptedValue,
   PathsJson = bindingContext.ValueProvider["PathsJson"].AttemptedValue
  };
  return model;
 }
}

-Controller.cs-
public ActionResult AddPath(PathModel path)
{
  if (!_service.AddPath(path))
  {
      path.SetModelValue(this.ModelState);
      return View("AddPath", path);
  }
  return RedirectToAction("Index");
}

上記の例では、Controller.AddPathは引数としてPathModelを受け取る。このPathModelはModelBinder属性が指定してあり、その指定でバインド処理をPathModelBinderで行うと明示してある。そのPathModelにはList<PathItemModel>というプロパティがあるのだが、その値はポストされたPathsJsonを解析して生成されるものなのでその処理をPathModelBinderで行っている。実際の処理はPathModelBinderのBindModelを見てもらえば一目瞭然なので省略する。

以上がカスタムModelBinderを使用するために必要な実装だ。どうだろう、かなりシンプルにできているのが実感できたと思う。これでController内に「値の変換を行う」という処理がなくなり、Controller本来の役目であるフローの制御に集中できるようなった。より関心の分離が進んだ状態というわけだ。

最後に一点注意が必要なのがPathModelのようにカスタムModelBinderを使用しているModelをViewへ渡す場合だ。その場合は、PathModelのSetModelValueで行っているようにModelStateへ値を設定してやらないとViewで値の取得ができないので忘れずに実装してほしい。

2009年11月12日木曜日

Google maps APIのGClientGeocoderの使い方

今回はGoogle maps APIのGClientGeocoderの使い方を解説する。GClientGeocoderはクライアントスクリプトから直接Googleの膨大な地図情報にアクセスできるという大変便利なクラスだ。

呼び出し方法は下記のようになる。

var geocoder = new GClientGeocoder();
var center = map.getCenter();
geocoder.getLocations(
 center, 
 function(response) {
  if (!response || response.Status.code != 200) {
   alert("データが取得できませんでした");
  }
  else {
   var place = response.Placemark[0];
   var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]); 
   // Code here...
   var address =  place.AddressDetails.Country.AddressLine[0];
   // Retrieve address for whatever you want to...
  }
});

getLocationsのオーバーロードとして、文字列の住所を受け取るものと、GLatLngを受け取るものと両方あるので用途に分けて使用して欲しい。

getLocationsで返却されるresponseの中身であるJSONクラスは下記になる(このページより抜粋)。

{
  "name": "1600 Amphitheatre Parkway, Mountain View, CA, USA",
  "Status": {
    "code": 200,
    "request": "geocode"
  },
  "Placemark": [
    {
      "address": "1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA",
      "AddressDetails": {
        "Country": {
          "CountryNameCode": "US",
          "AdministrativeArea": {
            "AdministrativeAreaName": "CA",
            "SubAdministrativeArea": {
              "SubAdministrativeAreaName": "Santa Clara",
              "Locality": {
                "LocalityName": "Mountain View",
                "Thoroughfare": {
                  "ThoroughfareName": "1600 Amphitheatre Pkwy"
                },
                "PostalCode": {
                  "PostalCodeNumber": "94043"
                }
              }
            }
          }
        },
        "Accuracy": 8
      },
      "Point": {
        "coordinates": [-122.083739, 37.423021, 0]
      }
    }
  ]
}

GClientGeocoderが独自のCacheメカニズムを実装しているので同じアドレスであればServerへのRound tripは発生しない。しかし、複数のアドレスを問い合わせるためにGClientGeocoderをループ内で複数回使用することは推奨されていないので、その場合はサーバ側などでHTTP Geocoderを使用するのがよいだろう。

2009年11月8日日曜日

Googleが提供するJavaScriptコンパイラ:Closure Compilerの紹介

GoogleがGmail、Google docs、Google mapsなどの開発用に使用しているツール群をオープンソース化するということで、その第一弾としてJavaScriptのコンパイラ:Closure Compilerが公開されたのでここで紹介する。

コンパイラと言っても実際にマシン語などに変換するわけではなく、より良いコードへと最適化してくれる、という表現のほうが正しい。また文法的な間違いも検出してくれるのでJavaScript開発者にとってはかなり嬉しいツールとなるだろう。

使い方は簡単でこのページにコードをペペッとはっつけてあとはCompileボタンを押下するだけ。これだけで右側に文法チェック後に最適化されたコードが出力される。エラーやらワーニングがなければペペッとコピーするなり「The code may also be accessed at <ファイル名>」からファイルをダウンロードすればよい。ちなみにこのファイルは一時間限りの限定的なものなので、開発時において一時的に直接参照するのは良いだろうが、本番環境にあるファイルから直接参照するようなことはやめよう。

また、上記で紹介したWeb上のツールも短いコードをテストする分には良いけれど、少しでも規模が大きくなってくると毎回の設定が面倒になってきてしまうだろうから、こちらのAPIを使用することが推奨されている。使ってみて勝手が分かってきたらこちらのほうもおいおい紹介しようと思っている。

今まではコードの軽量化のためにjsminのC#コードを少しいじって使用していたけれど、文法チェックまで行えるClosure Compilerのほうを今後はメインに使っていくことになるだろう。



追記 2019/1/17
とある筋からUIがもっと初心者フレンドリーなJavaScriptとCSSの圧縮サービスを紹介されたので合わせて紹介しておきます。

https://www.websiteplanet.com/ja/webtools/jscssminifier

少しJavaScriptをいじるだけの人や、開発が本職ではない人には上記のサイトの方が親切でわかりやすいので良いと思います。

ただ本職でゴリゴリ開発する人はhttps://webpack.js.org/とかを開発のフローに入れた方が効率は跳ね上がるのでそちらを参照してください。

2009年11月4日水曜日

Google mapsをブラウザ幅にあわせて伸縮させる方法

Google codeにあるGoogle mapsのサンプルそのままでは、地図をブラウザ幅にあわせて伸縮させた際にJavaScriptの解析エラーが出てしまう。今回はその対処方法を紹介する。

サンプルのコードは以下。
if (GBrowserIsCompatible()) {
 var map = new GMap2(document.getElementById("map_canvas"));
 map.setCenter(new GLatLng(37.4419, -122.1419), 13);
 map.setUIToDefault();
}

ちなみにエラーの理由はsetUIToDefault()にあるようなので、それをのけて下記のように手動で設定すればよい。

if (GBrowserIsCompatible()) {
 var  map = new GMap2(document.getElementById("map_canvas"));
 map.enableContinuousZoom();
 var ui = new GMapUIOptions();
 ui.maptypes = { normal: true, physical: true, satellite: true, hybrid: true };
 ui.zoom = { doubleclick: true, scrollwheel: true };
 ui.controls = { largemapcontrol3d: false, smallzoomcontrol3d: true, scalecontrol : true, maptypecontrol: false, menumaptypecontrol : true };
 map.setUI(ui);
}

ここでは、ダブルクリックでのズームアニメーションの有効化(enableContinuousZoom())、表示するマップ種類の設定(maptypes)、UI操作での地図ズーミングの設定(zoom)、地図上に表示するUIコントロールの設定(controls)などを行っているので参考にして欲しい。もちろん言うまでも無いことだが詳細情報はAPI Referenceを参照してほしい。

2009年10月26日月曜日

LinqToSqlでの開発の注意点 その1

LinqToSqlは.net framework3.5より導入されたORM(Object-relational mapping)だ。LinqToSql以前はORMといえばNHibernateなどが主流だったと思う。いまだにいくつかの点でNHibernateに譲るけれど(2nd Level Cacheやクエリーベースの取得など)、LinqToSqlのほうがよりシンプルな印象を持った。私見だがLinqToSqlを使用してのプロジェクト開発は、ORMを使用しないプロジェクトに比して段違いに開発スピードが速い。これから複数回に分けてLinqToSqlの注意点について解説していきたいと思う。

まず一番初めに直面するであろう問題はデータの更新時に訪れる。プロジェクトへdbmlファイルを追加し、必要なTableも追加し、データを取得、挿入までトントンと進むと思われるが、dbmlへ何も対処を施していないと挿入したデータを更新する際に下記の例外に直面する。

An entity can only be attached as modified without original state if it declares a version member or does not have an update check policy
(注意:実際に発生する例外は若干違うかもしれない。今回は実地に試す環境がないので思い出しながら解説しています)

ソースコードはこのようなものを想定している。

void Update(Cilp clip)
{
 var db = new MyClipDataContext();
 db.Clips.Attach(clip, true);
 db.SubmitChanges();
}

この例外は並列処理時の上書き更新を防ぐための処理が初期状態でdbmlに施されているために発生する。これを避けるためには各テーブルにTimeStamp列を追加し、更新を下記のように変更する必要がある。

void Update(Cilp clip)
{
 var db = new MyClipDataContext();
 var dbClip = db.Clips.Single(x => x.Id==clip.Id);
 dbClip.Value = clip.Value;
 db.SubmitChanges();
}

これでひとまず例外が発生することは無くなった。ただ上記の例では上書き更新を防いでいるとはいえないだろう。実際には下記のように使うのが正しい。ちなみにこのソースコードは一つ目の例と同じだ。ただClipsテーブル上にTimeStamp列が追加されているものとし、さらに(ここが重要なのだが)引数clipは新規にnewしたものではなく、DBから取得したデータをSerialize, Deserializeしたものとする。

void Update(Cilp clip)
{
 var db = new MyClipDataContext();
 db.Clips.Attach(clip, true);
 db.SubmitChanges();
}

これにより、データ取得時のTimeStampを損なうことなく正確な上書き防止処理が期待できる。

ただ、上書き防止などを必要としない処理を行っている場合は、このような実装を行う必要はなく、下記のようにdbml上で各列ごとにUpdateCheckをNeverに設定すればよい。



これでTimeStampなどを気にすることなく更新処理が行えるようになる。ここで注意しなければならないのは、SQL Server上でテーブルを更新した場合、その更新をdbmlに反映するには再度テーブルをdbmlにD&Dする必要がある(信じられないことにVS2008にはそれ以外にSQL Server上の変更をdbmlへ反映する手段が無い)。このときにUpdateCheckをNeverに変更する処理を忘れずに行うようにしよう。

2009年10月23日金曜日

ViewからControllerへ配列を渡す方法

前回はViewから任意のClassを渡す方法を解説したが、今回はViewからControllerへ配列を渡す方法を解説する。これは下記の図のように、複数の選択項目があり、選択された内容を配列として取得したい場合にかなり有効だ。



実際のコードを見てみよう(簡易化しているのでこのままでは動かない恐れがあるので適時修正してもらいたい)。

-Import.aspx-
<%foreach( var contact in Model.ContactList)
{%>
 <input type='checkbox' name='contacts' value='<%=contact.Id%>'/> <%=contact.Name%>
<%}%>

-ImportController.cs-
public ActionResult Import(int[] contacts)
{
 // code here...
}

ポイントはcheckboxのname属性の値を固定にしていることだ。これで実際のHtmlにはContactListのアイテム分だけname='contacts'というcheckboxが描画される。そしてFormのポスト時にASP.NET MVCフレームワークが選択されたcontactsのvalueをひとまとめにしてImportControllerのImportActionの引数int[]として設定してくれる。

2009年10月20日火曜日

ViewからControllerへ任意のclassを渡す方法

前回Viewに任意のclassを渡す方法を解説したが、今回はViewからControllerへ値を渡す際に任意のclassを渡す方法を解説する。

といってもASP.NET MVCフレームワークが提供するModelBinderのおかげで実際のコードは至極簡単だ。

-HomeController.cs-
[AcceptVerbs(Http.Post)]
public ActionResult Index(Clip clip)
{
 // code here...
}

ControllerのActionの引数をclassにしておくだけで、フレームワークがReflectionなどを駆使してポストされた値とActionの引数classのパラメータ名を比較してデータを格納していってくれる。

さらに、値を格納してくれるついでにclassのプロパティとポストされた内容を比較し、ポストされた内容に明らかな間違い(intプロパティに文字列が入力されていたり)や不足分があるようだとModelStateにErrorとして追加してくれるわけだ。これはかなりの親切設計なので大体は役に立つのだが、たまに邪魔になるときがある。そんなときのために、値の格納・検証を行わないようActionの引数classへ指定する方法がある。それが下記の属性だ。

-HomeController.cs-
[AcceptVerbs(Http.Post)]
public ActionResult Index([Bind(Exclude = "Id,Position")]Clip clip)
{
 // code here...
}

Clipクラスに指定されているBind属性を見て欲しい。上記の例は、Clip.IdとClip.Positionの二つのプロパティの格納・検証を無視するように指定している。これでポスト内容にIdとPositionがなくともModelStateにErrorが追加されることはなくなった。

2009年10月18日日曜日

Viewに任意のClassを渡す方法

今回はASP.NET MVCのViewへ任意のClassを渡す方法を解説する。

初期のASP.NET MVCプロジェクトに配置されている/Views/Home/Index.aspxを見てみよう。Index.aspxの初期の基本クラスはSystem.Web.Mvc.ViewPageとなる。このままでもViewDataを使用すればデータをControllerからViewへ渡すことは可能だ。

-HomeController.cs-
public ActionResult Index()
{
 ViewData["Message"] = "Hello";
 return View("Index");
}

-Index.aspx-
<%=(string)ViewData["Message"]%>

しかしこれではC#の原則である強固に型付けされたプログラミングが行えない。そのためデータを渡すたびになんらかのキャストが必要になってしまう。

なので基本クラスにGenericを使用し任意のClassを渡すように変更する。Index.aspxの冒頭の一行は下記のようになる。

-Index.aspx-
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyClip.Models.ClipModel>" %>

これでMyClip.Models.ClipModelをIndex.aspx上で参照することができるので、下記のようなプログラミングができる。

-HomeController.cs-
public ActionResult Index()
{
 var clip = new ClipModel(){ Name="MyClip.net" };
 return View("Index", clip);
}

-Index.aspx-
<%=Model.Name%>

これで型付けされたプログラミングが行える。C#はJavaScriptとは違い強固に型付けされたプログラミングがフレームワークの根底にあるのでModelを使用するほうが好ましいだろう。また、ViewDataの値を参照するたびにキャストが必要なプログラミングをするよりも型付けされたModelを参照するほうが可読性も保守性も高まる。

2009年10月17日土曜日

散歩道機能のリリース

My-Clip.netに散歩道を登録できる機能を追加した。ここから機能を確認できるので体験してもらえたらと思う。

画面的には下記のようになる。



これでお気に入りの散歩コースなどを登録できるし、それらのコースのおおまかな距離をはかったりもできるので色々と活用していただけたらと思う。

2009年10月16日金曜日

MSDTCの例外を直す方法

System.Threading.TransactionScopeを使用しTransactionを一元管理しようとする際に「MSDTCが”サーバ名”上で無効です」というような例外が発生する場合がある。
(上記エラーメッセージが日本語でどのように表示されるのかは不明だが英文だと次のように表示される。「MSDTC on server ’server name’ is unavailable」)

この問題は下記の手順で修正できる。

1、管理ツール → サービス
2、Distributed Transaction Cordinatorを右クリック → 開始

またプロパティからスタートアップの種類を自動に変更しておいてもよいだろう。

2009年10月13日火曜日

カスタムActionFilter その2

前回は認証を必要とするActionFilterを解説したが、今回はロールに準拠した動作をするカスタムActionFilterを解説する。

例として求人サイトを挙げる。求人サイトには求職者=Candidateと雇用者=Employerの2種類のユーザーロールが存在するものとする。そして、両者に共通の複数のページ(例:ホームページ、会社情報ページなど)があるものとし、それらのMasterPageをそれぞれのロールに準拠したものに変更したい場合の処理を下記に実装する。

-RoleBaseMasterPageAttribute.cs-
public class RoleBaseMasterPageAttribute : ActionFilterAttribute
{
public RoleBaseMasterPageAttribute()
  : base()
{
   EmployerMasterPage = "~/Views/Shared/Employer.Master";
   CandidateMasterPage = "~/Views/Shared/Candidate.Master";
   AnonymousMasterPage = "~/Views/Shared/Site.Master";
}

public string EmployerMasterPage { get; set; }
public string CandidateMasterPage { get; set; }
public string AnonymousMasterPage { get; set; }

public override void OnResultExecuting(ResultExecutingContext filterContext)
{
   ViewResult viewResult = filterContext.Result as ViewResult;
   if (viewResult == null)
     return;

   if (!filterContext.HttpContext.Request.IsAuthenticated)
  {
     viewResult.MasterName = AnonymousMasterPage;
  }
   else if (filterContext.HttpContext.User.IsInRole("Candidate"))
  {
     viewResult.MasterName = CandidateMasterPage;
  }
   else
  {
     viewResult.MasterName = EmployerMasterPage;
  }
}
}

これをActionまたはController全体に付与すれば、ログインしているユーザーのロールに準拠したマスターページを表示することができる。

2009年10月12日月曜日

カスタムActionFilter その1

ASP.NET MVCにはActionFilterという属性がある。これはController全体かまたは個々のActionに対して設定できるようになっている属性で、出力内容を指定の期間キャッシュするOutputCacheAttributeなどがある。今回はこれを使って便利なActionFilterを作ってみようと思う。

ASP.NET MVCのフレームワークにはすでにAuthorizeAttributeという認証していないユーザーのリクエストは無視するActionFilterがある。これはこれで役に立つ機能なのだが、リクエストをまったく無視されてもユーザーフレンドリーとは呼べないので、もっと便利に、認証していないユーザーのリクエストはログインを促すように変更したいと思う。

-RequiredAuthAttribute.cs-
[AttributeUsage(AttributeTargets.All, 
  AllowMultiple = true)]
public class RequiredAuthAttribute : ActionFilterAttribute
{
  public string Route{ get; set; }
  public string Action { get; set; }

public RequiredAuthAttribute()
{
  Route = "Account";
  Action = "Login";
}

private string RedirectUrl()
{
  if (Action.CompareTo("") != 0)
    return "/" + Route + "/" + Action;
  return "/" + Route;
}

public override void OnActionExecuting(
  ActionExecutingContext filterContext)
{
  if (filterContext == null)
  {
    throw new ArgumentNullException(
  "filterContext is null in the RequiredAuth attribute");
  }

  if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
  {
    string loginUrl = RedirectUrl();
    filterContext.HttpContext
     .Response.Redirect(loginUrl, true);
  }
}
}

ActionFilterAttributeを継承したRequiredAuthAttributeクラスのOnActionExecutingで、認証されていないリクエストの場合はリクエスト先のURLを変更している。ここでは/Account/Login/に転送している。

実際の使用方法は下記の通りだ。

-ClipController.cs-
[RequiredAuth]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Comment(int clipId, string commentText)
{
  // code here...
}

上記の例はCommentというActionに対するFilterになる。ActionFilterはController全体に対しても付与できる。

以上で既存のコード部分に手を加えるでなく、簡単に認証が必要なActionを実装することができた。このほかにもRoleベースのフィルタリングなども可能なので、ネタが続くだけ紹介していきたいと思う。

2009年10月9日金曜日

Routingの話 その3

以前カスタムRouteの話をしたけれど、今回はカスタムRoute用のURLを出力する方法を解説する。

ASP.NET MVCのフレームワークとしてあるアクション用のURLを出力するのにSystem.Web.Mvc.Html.ActionLink()メソッドが用意されている。しかし、複数のパラメータを渡す場合のカスタムRouteのためにHtml.ActionLink()メソッドでURLを生成すると、下記のような場合は予期せぬURLを出力する。
(実際にはActionLikでも期待したとおりのURLを出力できました:追記コメント)
(RouteLinkの紹介用にこのエントリは残しておきます:追記コメント)

Html.ActionLink("Next", 
  "Category", "Clip", 
  new { nameKey="futsal", time = "week", page="2" }, null)

ここではaのようなURLを期待しているのだが、実際にはbのURLが出力される。


a. http://www.my-clip.net/Clip/Category/futsal/week/2

b. http://www.my-clip.net/Clip/Category?nameKey=futsal&time=week&page=2


これでは期待したアクションを呼び出せないのでASP.NET MVCのソースコードを解析してみたところ、どうやらActionLinkメソッドに2つか3つ以上の引数を渡すと上記bのような形でしか出力してくれないようだ。そこで変わりにHtml.RouteLink()メソッドを使用する。記述方法は下記の通りだ。

Html.RouteLink("Next", 
  "CategoryWithPage", 
  new { nameKey = "futsal", time = "week", page = "2" })

ちなみに第二引数はGlobal.asaxでMapRouteメソッドで指定した名称を設定する。これで期待したURLが出力される。

ASP.NET MVC Ver1.0のソースコードはここからダウンロードできる。

-追記(2009-11-01)-
こちら等閑に付さないようにさんがこのエントリの間違いを指摘してくださっていたので間違いを修正。

2009年10月8日木曜日

jQueryとASP.NET MVCで非同期通信 その2

前回は非同期通信のサーバサイドをControllerクラスとしたけれど、今回はWeb Serviceをサーバサイドとして場合を解説する。

まずコメントを入力するtextareaと保存ボタンを用意する。

-details.aspx-
<div>
  <div>
    <textarea id="commentArea"></textarea>
  </div>
  <input id="saveButton" type="button" value="Save"/>
</div>

ついで、JavaScriptでsaveButtonのClickイベントをハンドルし、jQueryのajaxメソッドでサーバサイドと非同期通信を行う。ここまでは前回同様だが今回は接続先のurlが異なる。

-JavaScript-
$(document).ready(function() {
 $('#saveButton').click(function(){
  try {
   $.ajax({
              type: "POST",
              url: "http://www.my-clip.net/WebServices/Clip.asmx/Comment",
              data: { clipId: _clipId, commentText: $('#commentArea').val() },
              contentType: "application/json",
              dataType: "json",
              success: function(msg) {
                  // code here...
              },
              error: function(e) {
                  // code here...
              }
          });
  }
  catch (e) {
   // code here...
  }
 });
});

ここでUrlを指定する場合に注意が必要だ。上記の例はMyClipアプリケーションと同一ドメイン上にWeb Serviceを設置しているので正常に動作する。しかし、異なるドメイン(例:http://www.yahoo.co.jp/)へのリクエストはスクリプト上からはできない。これはほとんどのブラウザでドメインをまたいだリクエストが禁止されているからである。対処として、上記の例のようにWeb Serviceを同一ドメイン上に設置するか、またはいったん同一ドメインへリクエストをし、その後リクエストされた先から外部ドメインへリクエストする(ブリッジする、という)という手法があげられる。ちなみに、リクエスト先のWeb Serviceを運用しているアプリケーション上に設置すれば、http://www.my-clip.net/の部分は必要なく、/WebServices/Clip.asmx/Commentの部分のみでリクエストできる。

もう一点、ContentTypeをjsonに変更している。これはWeb Serviceのメソッドを記述する際にResponseFormatをJsonで指定しているからである。Web Serviceのメソッドは以下の通りだ。

-Clip.asmx-
[WebMethod(CacheDuration = 10)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public CommentModel Comment(
  int clipId, 
  string commentText)
{
  // code here...
}

以上で一連の流れは終了だ。サーバサイドとしてWeb Serviceを使用してもクライアント側のコードにほぼ違いは無い。ただWeb Serviceを使用する場合は設置場所に注意が必要だ。

2009年10月7日水曜日

jQueryとASP.NET MVCで非同期通信 その1

ASP.NET MVCは非同期通信を行うためのフレームワークとしてAjax.BeginForm()とPartialView()が用意されている。これらはDavid HaydenのBlogに詳しく載っているのでそちらを参照して欲しい。

今回はjQueryを使った非同期通信を解説する。また例によってここで記載するサンプルコードはMyClipで実装したコメント投稿を簡易化したものになる。

まずコメントを入力するtextareaと保存ボタンを用意する。

-details.aspx-
<div>
  <div>
    <textarea id="commentArea"></textarea>
  </div>
  <input id="saveButton" type="button" value="Save"/>
</div>

ついで、JavaScriptでsaveButtonのClickイベントをハンドルし、jQueryのajaxメソッドでサーバサイドと非同期通信を行う。

-JavaScript-
$(document).ready(function() {
 $('#saveButton').click(function(){
  try {
   $.ajax({
              type: "POST",
              url: "/Clip/Comment",
              data: { clipId: _clipId, commentText: $('#commentArea').val() },
              contentType: "application/x-www-form-urlencoded",
              dataType: "json",
              success: function(msg) {
                  // code here...
              },
              error: function(e) {
                  // code here...
              }
          });
  }
  catch (e) {
   // code here...
  }
 });
});

ここで注意が必要なのがcontentTypeの指定だ。application/jsonを指定するとなぜだか引数が常にnullになる。

ClipControllerの処理は以下の通りだ。

-ClipController.cs-
public ActionResult Comment(
   int clipId, 
   string commentText)
{
 // saving data code here...

 return Json(new
 {
  // return data here...
 });
}

以上で一連の流れは終了だ。いたって簡単に非同期通信を行えるのが分かるだろう。またController側も特別な記述をとくに必要としない。次回はControllerのActionではなく、WebServiceを呼び出す方法を解説する。

2009年10月5日月曜日

Routingの話 その2

前回Routingの基礎を解説したので今回はカスタムRoutingを解説する。

初期のRoutingとしてGlobal.asaxに以下のRouteが登録されている。

routes.MapRoute(
 "Default", 
 "{controller}/{action}/{id}",
 new { controller = "Home", action = "Index", id = ""}
);

MapRouteメソッドの第二パラメータに注目して欲しい。これは下記のようなリクエストを想定している。

http://www.my-clip.net/Clip/Edit/2

ClipControllerのEditメソッド、Id=2というリクエストになる。ちなみに複数パラメータを渡す場合はどのようなURLになるかというのは前回説明した通りだ。では、複数のパラメータを渡す場合に下記のようにするにはどうしたらよいだろうか。

http://www.my-clip.net/Clip/Category/futsal/week/2

Global.asaxに次のコードを追加すればよい。

routes.MapRoute(
 "CategoryWithPage", 
 "Clip/Category/{nameKey}/{time}/{page}", 
 new { controller = "Clip", 
  action = "Category", 
  time = "week", 
  page = 1 }
);

そしてClipControllerの記述は下記のようになる。

public ActionResult Category(
 string time, 
 string nameKey, 
 int page)
{
  // Code here...
}

これで読みやすいURLをユーザーに提供することができる。次の二つのURLを比べれば後者のほうが理解しやすいのは一目瞭然だろう。

http://www.my-clip.net/Clip/Category?nameKey=futsal&time=week&page=2

http://www.my-clip.net/Clip/Category/futsal/week/2

ちなみにMapRouteメソッドの第三パラメータはデフォルト値なので、http://www.my-clip.net/というようにController、Action部分が省略されてリクエストされた場合は第三パラメータが使われる。なのでhttp://www.my-clip.net/はhttp://www.my-clip.net/Home/Index/と等価だ。

2009年10月4日日曜日

Routingの話 その1

今回はASP.NET MVCのRoutingの基本的な話。Routingとは何ぞやというと、リクエストされたURLと実際の処理をマッピングしてくれる処理になる。ASP.NET MVCはControllerとViewが完全に分離されているので、Routingが必要になるわけだ。実際に例を見たほうが理解しやすいので一緒に見ていこう。

http://www.my-clip.net/Account/Login/

このURLはAccountControllerのLoginというActionを要求している。ActionというのはControllerの実際の動作にあたる。ControllerのpublicメソッドはすべてActionになる。つまり外部から呼び出し可能ということになるので注意が必要だ。

では実際にAccountControllerのLoginメソッドを見てみよう。

public ActionResult Login()
{
   return View("Login");
}

Loginメソッドの戻り値としてLoginViewを返却している。このLoginViewというのは、/Views/Account/Login.aspxを指している。ちなみにLoginメソッドの返却値を以下のように変更してもASP.NET MVC的にはまったく問題が無い。

public ActionResult Login()
{
   return View("AnotherLogin");
}

ただし、/Views/Account/AnotherLogin.aspxが存在することが必須だ。

このようにASP.NET MVCは実際のaspxを隠蔽しているのでエンドユーザーはその存在をうかがい知れない。ここがMVCと銘打たれた所以だろう。ちなみにIdなどのパラメータを渡したい場合は下記のようになる。

http://www.my-clip.net/Clip/Details/23

23の部分がIdにあたる。上記の例は、ClipControllerのDetailsActionをId=23で要求している。複数のパラメータを渡したい場合は下記のようになる。

http://www.my-clip.net/Account/Login?username=yokyo&preference=japanese

これで大体Routingがどういうものか理解できたかと思う。次回はカスタムRoutingを解説したいと思う。

2009年10月3日土曜日

Googleストリートビューの実装方法

下記の画像のようにMyClipにGoogleストリートビューの機能を盛り込んだので、その開発手順をここで紹介する。動作確認は実際にMyClip上で行って欲しい。今回の記事はストリートビューを使うにあたってすでにGoogle Maps APIのGMap2クラスに多少なりとも親しんでいるものとして話を進める。
APIの詳細はGoogle Maps API Referenceを参照してほしい。



-概要-
クリックされた地図上のストリートビューのデータ取得のためにGStreetviewPanorama.getNearestPanorama()を使用する。その際に問題になるのがクリックされた場所。そこが道路上ならば問題ないけれど建物上であればgetNearestPanorama()でデータを取得することができない。それなのでまず最初にGDirections.loadFromWaypoints()で最寄の道路座標データを取得する。

以下のコードは必要なところだけを抜き出した簡易版なので、手順だけでも大まかに掴んでもらえたらと思う。
(MyClip上ではストリートビュー関連の処理をクラスとしてまとめてあるので、thisはそのクラスを表すもの)

//マップの準備
this._map = new GMap2(
  document.getElementById("map"));
this._map.setUIToDefault();
this._map.setCenter(
  new GLatLng(lat, lng), 17);

//ビューの地図上の場所を表すアイコンの準備
this._guyIcon = new GIcon(G_DEFAULT_ICON);
this._guyIcon.image = 
  "http://maps.gstatic.com/mapfiles/cb/man_arrow-0.png";
this._guyIcon.transparent = 
  "http://maps.gstatic.com/mapfiles/cb/man-pick.png";
this._guyIcon.imageMap = [
  26, 13, 30, 14, 32, 28, 27, 28, 
  28, 36, 18, 35, 18, 27, 16, 26,
  16, 20, 16, 14, 19, 13, 22, 8];
this._guyIcon.iconSize = new GSize(49, 52);
this._guyIcon.iconAnchor = new GPoint(25, 35);

//ストリートビューのレイヤーをマップ上に載せる
this._svOverlay = new GStreetviewOverlay();
this._map.addOverlay(this._svOverlay);

//ストリートビューを準備
this._streetViewClient = new GStreetviewClient();
this._streetViewer = new GStreetviewPanorama(
  document.getElementById("stview"));

//最寄の道路座標を取得の準備
//loadFromWaypointsは非同期でデータ取得を行うので、callbackメソッドを設定する必要がある。ここではGEventにGDirectionsのloadイベントを登録している
this._direction = new GDirections();
GEvent.addListener(
 this._direction, "load", function() {
  //これで最寄のデータ取得完了
  var p = this._direction.getPolyline().getVertex(0); 
  var mystv = this;

  // 取得した道路上のストリートビューデータを取得
  this._streetViewClient.getNearestPanorama(p, 
   function(panoData) { mystv._showPanoData(panoData); });
});

// getNearestPanorama()のcallbackメソッド
_showPanoData: function (panoData) {
 this._streetViewer.setLocationAndPOV(
   panoData.location.latlng);
 if (!this._guyMarker) {
  this._guyMarker = 
   new GMarker(panoData.location.latlng, 
    { icon: this._guyIcon, draggable: true });
  this._map.addOverlay(this._guyMarker);
  var mystv = this;
  GEvent.addListener(this._guyMarker, "dragend", 
   function() { mystv._onDragEnd(); });
 }
 else {
  this._guyMarker.setLatLng(panoData.location.latlng);
 }
}

// 地図上の人型アイコンのドラッグ終了イベントハンドラ
_onDragEnd: function() {
 var latlng = this._guyMarker.getLatLng();
 this._direction.loadFromWaypoints(
  [latlng.toUrlValue(6), latlng.toUrlValue(6)], 
  { getPolyline:  true });
}

これで地図上にストリートビューのレイヤーと人型アイコンが表示され、人型アイコンをドラッグ&ドロップすれば最寄の道路上のストリートビューが表示される。

このほかにもGStreetviewPanoramaのinitialized, yawchangedイベントを実装すれば、ストリートビュー上のユーザーアクションが拾える。そうすれば地図からストリートビュー、またその逆へ、という双方向な挙動が実現できるのでよりユーザーフレンドリーな地図サービスが提供できる。デモを漁れば前述のイベント処理を実装したサンプルが見つかるので興味がある方はそちらへ。

2009年9月30日水曜日

IE6とIE8のスタイルシートの違い

前回はいかにIE8がファジーで勝手に期待通りの挙動をしてくれるかの話をしたのだが、今回はIE6のスタイルシートを少し解説する。

「みなさん、これが私のサイトです。よろしかったら使ってみてください」と友人・知人に触れ回ったところ、それでものが動かないというのはかなり恥ずかしいもので、ことにIEはVersion毎にCSSの解釈にかなりの差異があるので注意が必要だ。とくにIE6には注意しよう。

実際にこの統計からIE6の使用者は2009年8月時点でまだ13.6%もいる。徐々に減りつつあるとはいえ、最新版のIE8よりもシェアが多い。無視できない割合だ。

実際に何が問題なのかは次の画像を見比べてほしい。

こっちはIE8。


これはIE6(IE8では描写されている画像上部のタブが無い)。


こっちはIE8。


これはIE6(投票するための上下の矢印が無い)。


全体的にCSSが機能していないのが見て取れる。なぜかというのを調べてみたらどうやら下記のようなCSSの記述方法に問題があるらしい。

div>span
{
 font-weight:bold;
}

このspanの部分、div要素の子要素を指定しているspanの部分が機能していない。IE6ではこのような記述を解釈しないようだ。回避方法としては・・・、そのうち調査します。

2009年9月29日火曜日

ブラウザによるjQueryの挙動の違い

ブラウザ間の挙動の違い、とことさらに騒ぎ立てるようなことではないのだが、今回はIEをメインにWeb開発していると、FireFox、Chromeを使ってテストをする際に予期せぬ挙動に出くわすというお話。

動作確認したブラウザのVersion
IE8, FF3.5.3, Chrome3


1、val()とtext()
val()はinput要素のvalueを取得するための関数で、text()はspanやdiv要素などのinnerTextを取得する用。なので下記の記述は間違い、・・・になるはずなのだがIE上では正しく値が取得できてしまう。

// textboxから値を取得(IEだけで動くVer)
var hoge = $('input:text').text();

しかし、これではFF, Chrome上では期待した動作をしない(値が常にempty)ので下記のように訂正。

// textboxから値を取得(全部で動くVer)
var hoge = $('input:text').val();

上記の記述方法でも、もちろんIEは期待通りの動作をする。


2、selectorの厳密さ
これもかなりIEがいい加減だな、という話なのだけれど、ある要素のオブジェクトを取得する際の挙動の違い。

// classにselectedを指定しているspan要素を取得(IEだけで動くVer)
var hoge = $("span[class='selected'").val();

上記、IEでは動くけれど、FF、Chrome上では要素を取得できない。なぜかというと、spanの属性を指定している部分の閉じタグが抜け落ちているからである。これは単純なバグなので発見も容易だと思うけれど、複雑な処理を記述している中でこういうことがあると、コンパイラのないJavaScriptではデバッグするのにかなりの苦労を伴うと思う。実際には下記が正しい。

// classにselectedを指定しているspan要素を取得(全部で動くVer)
var hoge = $("span[class='selected']").val();



というふうに、IEはかなりファジーな挙動をするのでリリース前には他ブラウザでテストを入念に行う必要がある。

最後にFFでScriptをデバッグするのに大いに役立つAdd-onを紹介しておこうと思う。Firebug

My-Clip.netをリリース

情報収集系サイトとしてwww.my-clip.netをリリースしました。このサイトは、各地域の店舗情報とユーザーのUpVote、DownVoteで表される好みを、Googleマップに載せて色々できたらなぁ、という考えから作られました。まだ基本的な機能のみなので、今後も随時機能追加していく予定です。