長い投稿は申し訳ありません。質問があります。ただ我慢してください。
小さなコンテキスト
私たちは、さまざまなユーザー設定、ユーザーが所属するグループ、ユーザーの出身地などに基づいて大幅に適応する必要があるサイトを持っています。以前はページのモデルに関連するビットを含めていたため、ページにユーザーが特定の年齢を超えているかどうかを示すテーブルがある場合、モデルでは次のようにします。
//model
public PageModel
{
public bool ShowTable {get;set;}
}
//controller
public PageController
{
public ActionResult ShowPage()
{
var model = new PageModel() {
ShowTable = User.Age > 21
};
return View(model);
}
}
//view
@if(Model.ShowTable)
{
<table>Some Html here</table>
}
これは、どのユーザーに何を表示する必要があるかを知るためにすぐに非常に複雑になりました。この問題に対処するために、特定のアイテムをいつ表示または非表示にするかに関するすべてのロジックを一元化しました。私たちはこのクラスをUserConfiguration
と呼び、(ほとんどの場合)何を表示すべきかを示すブール値を返す一連の関数を含んでいました。これにより、ユーザーに表示する必要がある一連の仕様とテストを設定できました。次に、このUserConfigratuion
は、すべてのページモデルが継承する必要がある基本クラスに配置されました。そのため、現在のところは次のようになります。
//UserConfiguration
public UserConfiguration
{
private readonly User _user;
public UserConfiguration(User user) {
_user = user
}
public bool ShowTable() {
return _user.Age > 21;
}
}
//model base
public ModelBase
{
public UserConfiguration {get;set;}
}
//model
public PageModel : ModelBase
{
// whatever data is needed for the page
}
//controller
public PageController
{
public ActionResult ShowPage()
{
var userConfiguration = new UserConfiguration(User);
var model = new PageModel {
UserConfiguration = userConfiguration
};
return View(model);
}
}
//view
@if(Model.UserConfiguration.ShowTable())
{
<table>Some Html here</table>
}
これは主に、ユーザーが表示すべきものと表示すべきでないものの一連のテストを作成できるために役立ちました。ただし、この追加のクラスをまとめてモデルに含める必要があるため、あまりクリーンなソリューションではありません。また、部分ビューのレンダリングにも影響があります。モデルにIEnumerable<Foo> Foos
プロパティがあり、パーシャルでレンダリングしたいが、そのパーシャルまたユーザー設定に依存している場合、問題が発生します。パーシャルはUserConfiguration
にアクセスできないため、モデルとしてパーシャルにfoosを渡すことはできません。したがって、この情報にアクセスするための最良の方法は何でしょうか。私がそれを見る方法、asp.net MVCのコンテキストでは、4つの方法が利用可能です。
1)パーシャルの新しいモデルを用意します。
// parent view
@{
var foosModel = new foosModel {
Foos = Model.Foos,
UserConfiguration = Model.UserConfiguration
}
}
@Html.RenderPartial("FooList", foosModel)
// child partial view
@if(Model.UserConfiguration.ShowTable) {
foreach(var foo in Model.Foos) {
//Some HTML
}
}
これはおそらく「最も純粋な」ソリューションであり、MVCの原則を最も忠実に守っていますが、多くの(おそらく不必要な)モデルを含み、プロジェクトの肥大化を引き起こしています。
2)ViewDataを介してUserConfigurationを公開します。例:
// parent view
@Html.RenderPartial("FooList", Model.Foos, new ViewDataDictionary { { "UserConfiguration", Model.UserConfiguration } })
// child partial view
@{
var userConfig = (UserConfiguration)ViewData["UserConfiguration"];
}
@if(userConfig.ShowTable) {
foreach(var foo in Model) {
//Some HTML
}
}
これはタイプセーフではなく、ViewDataから取得するためにマジックストリングに依存しているので、私はこれが本当に好きではありません。
3)UserConfigurationをViewBagに配置します。上記と同じ問題は本当に
4)ページモデルを変更し、ページ自体のプロパティを介してUserConfigurationを公開します http://haacked.com/archive/2011/02/21/changing-base-type-of-a -razor-view.aspx /
UserConfigurationはアンビエントコンテキスト情報であるため、上記のオプション4のようにクラスを介して公開することは理にかなっています。この種のデータを公開するためにMVCで一般に受け入れられているベストプラクティスはありますか?誰かが過去にオプション4のようなことを試みたことがありますか?
tl; dr:一般的なMVCまたは特にasp.net MVCで、コンテキスト情報をサイトのビューに公開する最良の方法は何ですか?
あなたは#5で行くべきです:上記のどれも。
IPrincipal
インターフェースの拡張メソッドの作成を開始しました。これにより、現在のユーザーが実行できることの厳密に型指定された宣言が得られます。これらの拡張メソッドで使用するためにセッションに入れるUserConfiguration
DTOを作成することもできます。
まず、拡張メソッド:
namespace YourApplication.Helpers
{
public static class UserConfigurationExtensions
{
private HttpContext CurrentContext
{
get
{
return System.Web.HttpContext.Current;
}
}
private static UserConfiguration Config
{
get
{
if (CurrentContext == null)
return null;
return CurrentContext.Session["UserConfiguration"] as UserConfiguration;
}
}
public static bool CanViewTable(this IPrincipal user)
{
return Config.ShowTable;
}
}
}
ユーザーが正常にログインしたら、UserConfiguration
のインスタンスを作成し、Session
に格納します。
public class AccountController : Controller
{
[HttpPost]
public ActionResult Login(LoginFormModel model)
{
if (ModelState.IsValid)
{
// Log in
Session["UserConfiguration"] = new UserConfiguration(...);
}
return RedirectToAction("Index", "Home");
}
}
次に、拡張メソッドが存在する名前空間をRazorテンプレートのデフォルトの名前空間に追加します。
YourApplication/Views/Web.config
<?xml version="1.0"?>
<configuration>
<!-- ... -->
<system.web.webPages.razor>
<namespaces>
<add namespace="YourApplication.Helpers"/>
</namespaces>
</system.web.webPages.razor>
<!-- ... -->
</configuration>
次に、Visual Studioソリューションを閉じて再度開きます。次に、Razorテンプレートに新しいメソッドが利用可能になります。
@if (User.CanViewTable())
{
foreach(var foo in Model)
{
//Some HTML
}
}
調査する必要があるほとんどの情報はユーザー中心であり、アクションベースではないようです。セッションにUserConfigurationを保存しないのはなぜですか?ユーザー認証/管理の方法に応じた別のアプローチは、ClaimsPrincipal(以下のサンプル)に必要なすべての情報を保持することです...
private ClaimsPrincipal CurrentClaimsPrincipal
{
get { return System.Security.Claims.ClaimsPrincipal.Current; }
}
public string Firstname
{
get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.GIVEN_NAME_KEY)?.Value : string.Empty; }
}
public string Lastname
{
get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.FAMILY_NAME_KEY)?.Value : string.Empty; }
}
public string AccessLevel
{
get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.ACCESS_LEVEL_KEY)?.Value : string.Empty; }
}
私は最初のオプションで、モデルクラスに必要な変更を加えます。オプション4、ページモデルの変更は、かみそりビューでヘルパー参照とモデル参照を交換することになるようです。オプション1は、必要なコードが少ないように見え、より多くのMVC開発者がそれを理解できるため、オプション4よりも保守が簡単なようです。
私の意見では、コンテキストデータサービスによって構成されたViewBag/ViewDataのコンテキストデータです。すべてのものが必要ない新しい薄いビューを追加する必要がない限り、すべてのビューがそれを必要とするため、「BaseController」が「GodModel」からすべてのものを設定する柔軟性がないか、非常に悪いことに悩みました。もちろん、「GodModel」はビューのモデルの基本クラスでもありました。
"all"ビューで本当に必要なものがあるかどうかを事前に判断することは困難であり、多くのものを必須にすることは、ものをオプションにすることよりもはるかに難しく、たまに忘れてしまいます動的なので設定してください。
もちろん、すべてのビュー固有で本当に必須なものはモデル化し、強く型付けして検証する必要があります。しかし、誰かが"すべてを強く型付けする必要がある"と思ったためにパフォーマンスを低下させる可能性のある一般的なものは、ナイスではありません。