asp.Netmvcソース分析-editorFor
私たちのmvcの日常開発では、LabelFor、EditorFor、Editorなど、この拡張方法には似ているものがたくさんあります.ここではEditorForで話しましょう.これは比較的複雑だと思います.
まず、EditorForの定義を見てみましょう.
public static MvcHtmlString EditorFor(this HtmlHelper html, Expression> expression, object additionalViewData) { return TemplateHelpers.TemplateFor(html, expression, null/* templateName */, null/* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData); }
EditorForには多くの定義があるが、実際にはTemplateHelpersを呼び出す.TemplateForメソッド.
internal static MvcHtmlString TemplateFor(this HtmlHelper html, Expression> expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData) { return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper)); 今皆さんはTemplateFor方法の主なパラメータがどれらがあるか知っているでしょうが、実際の開発では私たちのtemplateName、htmlFiledName、additionalViewDataは通常nullで、modeはDataBoundControl Modeです.Edit例を挙げてみましょう
public class UserInfo { [StringLength(100, MinimumLength = 10)] [Required] public string UserName { set; get; } }
@Html.EditorFor(model => model.UserName)
このコードは簡単ではありませんか.
では、TemplateForの実装を見てみましょう.
return templateHelper(html, ModelMetadata.FromLambdaExpression(expression, html.ViewData), htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, mode, additionalViewData);
まずModelMetadataを見てみましょうFromLambdaExpression(expression,html.ViewData)という文は、ModelMetadataを取得する方法です.具体的には、次のようになります.
この図を見て、return GetMetadataFromProvider(modelAccessor,typeof(TValue)、propertyName、containerType);このコードの中のパラメータはみんな着いたと信じています.
propertyName=“UserName” containerType=MvcApp.Controllers.UserInfo,modelAccessorはインスタンスを作成し,インスタンスの作成はmodel=>modelである.UserNameという言葉.GetMetadataFromProviderという方法については何も言うことはありませんが、前の記事ではすでに述べていますが、実際にはDataAnnotationsModelMetadataインスタンスを作成しています.
エクスプレスについてはGetExpressionText(expression)という言葉は、デフォルトでは属性名を返します.具体的には、次のようになります.
else if (part.NodeType == ExpressionType.MemberAccess) { MemberExpression memberExpressionPart = (MemberExpression)part; nameParts.Push("."+ memberExpressionPart.Member.Name); part = memberExpressionPart.Expression; }
2回目の実行
else if (part.NodeType == ExpressionType.Parameter) { //Dev10 Bug #907611 //When the expression is parameter based (m => m.Something...), we'll push an empty //string onto the stack and stop evaluating. The extra empty string makes sure that //we don't accidentally cut off too much of m => m.Model. nameParts.Push(String.Empty); part = null; }
もちろん、このメソッドのデフォルトの戻り結果はここでUserNameであり、htmlがid属性とname属性の値を生成するのがデフォルトです.
今からTemplateHelperの方法を見てみましょう
return
executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions);方法.
では、ExecuteTemplateの方法を見てみましょう.
internal static Dictionary> GetDefaultActions(DataBoundControlMode mode) {
return mode == DataBoundControlMode.ReadOnly ? defaultDisplayActions : defaultEditorActions;
}
GetView Nameは、viewの名前を取得する方法の1つです.
ではExecuteTemplateメソッドに戻ります
string fullViewName = modeViewPath + "/"+ viewName;これは私たちのビューが見つかりました.もし私たちが以前actionCacheにキーを含んでいたら、直接ビューを実行して戻り、次にView Enginesを通じて戻ります.Engines.FindPartialViewがビューを探しに来て、見つかったらビューを出力して返します.そうでなければデフォルトの処理を呼び出す
if (defaultActions.TryGetValue(viewName, out defaultAction)) { actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction }; return defaultAction(MakeHtmlHelper(html, viewData)); }
ここでのdefaultAction対応はDefaultDisplayTemplates.StringTemplateは、私のviewNameがStringなので、ここでのMakeHtmlHelperメソッドは、現在のViewContextとviewDataに基づいて新しいHtmlHelperをインスタンス化します.
DefaultEditorTemplates.StringTemplateメソッドは非常に簡単です.
return html.TextBox(String.Empty, html.ViewContext.ViewData.TemplateInfo.FormattedModelValue, CreateHtmlAttributes("text-box single-line")).ToHtmlString();
主にTextBoxメソッドを呼び出します.
public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary htmlAttributes) { return InputHelper(htmlHelper, InputType.Text, null, name, value, (value == null)/* useViewData */, false/* isChecked */, true/* setId */, true/* isExplicitValue */, htmlAttributes); }
ここのInputHelperはhtml文字列を本当に生成する場所です.この方法は全体的にわかりやすいですが、この方法にはこうあることに注意してください.
tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
これは、図のようなオブジェクト上の検証プロパティを処理することです.
皆さんはここまでEditorForの方法について知っていると思いますが、複雑な感じがします.
まず、EditorForの定義を見てみましょう.
public static MvcHtmlString EditorFor(this HtmlHelper html, Expression> expression, object additionalViewData) { return TemplateHelpers.TemplateFor(html, expression, null/* templateName */, null/* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData); }
EditorForには多くの定義があるが、実際にはTemplateHelpersを呼び出す.TemplateForメソッド.
internal static MvcHtmlString TemplateFor(this HtmlHelper html, Expression> expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData) { return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper)); 今皆さんはTemplateFor方法の主なパラメータがどれらがあるか知っているでしょうが、実際の開発では私たちのtemplateName、htmlFiledName、additionalViewDataは通常nullで、modeはDataBoundControl Modeです.Edit例を挙げてみましょう
public class UserInfo { [StringLength(100, MinimumLength = 10)] [Required] public string UserName { set; get; } }
@Html.EditorFor(model => model.UserName)
このコードは簡単ではありませんか.
では、TemplateForの実装を見てみましょう.
return templateHelper(html, ModelMetadata.FromLambdaExpression(expression, html.ViewData), htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, mode, additionalViewData);
まずModelMetadataを見てみましょうFromLambdaExpression(expression,html.ViewData)という文は、ModelMetadataを取得する方法です.具体的には、次のようになります.
public static ModelMetadata FromLambdaExpression(Expression> expression,
ViewDataDictionary viewData) {
if (expression == null) {
throw new ArgumentNullException("expression");
}
if (viewData == null) {
throw new ArgumentNullException("viewData");
}
string propertyName = null;
Type containerType = null;
bool legalExpression = false;
// Need to verify the expression is valid; it needs to at least end in something
// that we can convert to a meaningful string for model binding purposes
switch (expression.Body.NodeType) {
// ArrayIndex always means a single-dimensional indexer; multi-dimensional indexer is a method call to Get()
case ExpressionType.ArrayIndex:
legalExpression = true;
break;
// Only legal method call is a single argument indexer/DefaultMember call
case ExpressionType.Call:
legalExpression = ExpressionHelper.IsSingleArgumentIndexer(expression.Body);
break;
// Property/field access is always legal
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression)expression.Body;
propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null;
containerType = memberExpression.Expression.Type;
legalExpression = true;
break;
// Parameter expression means "model => model", so we delegate to FromModel
case ExpressionType.Parameter:
return FromModel(viewData);
}
if (!legalExpression) {
throw new InvalidOperationException(MvcResources.TemplateHelpers_TemplateLimitations);
}
TParameter container = viewData.Model;
Func
Editorを呼び出した場合、フォース呼び出しはFromLambdaExpressionメソッドではなくFromStringExpressionメソッドであり、この2つのメソッドの差は大きくありません.FromLambdaExpressionという方法を見てみましょう.この図を見て、return GetMetadataFromProvider(modelAccessor,typeof(TValue)、propertyName、containerType);このコードの中のパラメータはみんな着いたと信じています.
propertyName=“UserName” containerType=MvcApp.Controllers.UserInfo,modelAccessorはインスタンスを作成し,インスタンスの作成はmodel=>modelである.UserNameという言葉.GetMetadataFromProviderという方法については何も言うことはありませんが、前の記事ではすでに述べていますが、実際にはDataAnnotationsModelMetadataインスタンスを作成しています.
エクスプレスについてはGetExpressionText(expression)という言葉は、デフォルトでは属性名を返します.具体的には、次のようになります.
public static string GetExpressionText(LambdaExpression expression) {
// Split apart the expression string for property/field accessors to create its name
Stack nameParts = new Stack();
Expression part = expression.Body;
while (part != null) {
if (part.NodeType == ExpressionType.Call) {
MethodCallExpression methodExpression = (MethodCallExpression)part;
if (!IsSingleArgumentIndexer(methodExpression)) {
break;
}
nameParts.Push(
GetIndexerInvocation(
methodExpression.Arguments.Single(),
expression.Parameters.ToArray()
)
);
part = methodExpression.Object;
}
else if (part.NodeType == ExpressionType.ArrayIndex) {
BinaryExpression binaryExpression = (BinaryExpression)part;
nameParts.Push(
GetIndexerInvocation(
binaryExpression.Right,
expression.Parameters.ToArray()
)
);
part = binaryExpression.Left;
}
else if (part.NodeType == ExpressionType.MemberAccess) {
MemberExpression memberExpressionPart = (MemberExpression)part;
nameParts.Push("." + memberExpressionPart.Member.Name);
part = memberExpressionPart.Expression;
}
else if (part.NodeType == ExpressionType.Parameter) {
// Dev10 Bug #907611
// When the expression is parameter based (m => m.Something...), we'll push an empty
// string onto the stack and stop evaluating. The extra empty string makes sure that
// we don't accidentally cut off too much of m => m.Model.
nameParts.Push(String.Empty);
part = null;
}
else {
break;
}
}
// If it starts with "model", then strip that away
if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase)) {
nameParts.Pop();
}
if (nameParts.Count > 0) {
return nameParts.Aggregate((left, right) => left + right).TrimStart('.');
}
return String.Empty;
}
ここでのループは2回実行され、1回目は実行されます.else if (part.NodeType == ExpressionType.MemberAccess) { MemberExpression memberExpressionPart = (MemberExpression)part; nameParts.Push("."+ memberExpressionPart.Member.Name); part = memberExpressionPart.Expression; }
2回目の実行
else if (part.NodeType == ExpressionType.Parameter) { //Dev10 Bug #907611 //When the expression is parameter based (m => m.Something...), we'll push an empty //string onto the stack and stop evaluating. The extra empty string makes sure that //we don't accidentally cut off too much of m => m.Model. nameParts.Push(String.Empty); part = null; }
もちろん、このメソッドのデフォルトの戻り結果はここでUserNameであり、htmlがid属性とname属性の値を生成するのがデフォルトです.
今からTemplateHelperの方法を見てみましょう
internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate) {
// TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because
// the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically
// decorates it with a [ReadOnly] attribute...
if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model)) {
metadata.Model = null;
}
object formattedModelValue = metadata.Model;
if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly) {
formattedModelValue = metadata.NullDisplayText;
}
string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString;
if (metadata.Model != null && !String.IsNullOrEmpty(formatString)) {
formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model);
}
// Normally this shouldn't happen, unless someone writes their own custom Object templates which
// don't check to make sure that the object hasn't already been displayed
object visitedObjectsKey = metadata.Model ?? metadata.RealModelType;
if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey)) { // DDB #224750
return String.Empty;
}
ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData) {
Model = metadata.Model,
ModelMetadata = metadata,
TemplateInfo = new TemplateInfo {
FormattedModelValue = formattedModelValue,
HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName),
VisitedObjects = new HashSet
この方法は、実際には簡単であり、現在のmodelの値とhtmlを表示するformatフォーマットを取得し、最後にここでViewDataDictionaryインスタンスviewDataを新たに作成し、パラメータのadditionalViewDataもこのviewDataに統合し、現在の値(visitedObjectsKey、すなわちtextboxに最後に表示されるvalue)をviewDataのVisitedObjects属性に追加する.最後の再呼び出しreturn
executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions);方法.
では、ExecuteTemplateの方法を見てみましょう.
internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions) {
Dictionary actionCache = GetActionCache(html);
Dictionary> defaultActions = getDefaultActions(mode);
string modeViewPath = modeViewPaths[mode];
foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName)) {
string fullViewName = modeViewPath + "/" + viewName;
ActionCacheItem cacheItem;
if (actionCache.TryGetValue(fullViewName, out cacheItem)) {
if (cacheItem != null) {
return cacheItem.Execute(html, viewData);
}
}
else {
ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName);
if (viewEngineResult.View != null) {
actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName };
using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) {
viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
return writer.ToString();
}
}
Func defaultAction;
if (defaultActions.TryGetValue(viewName, out defaultAction)) {
actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction };
return defaultAction(MakeHtmlHelper(html, viewData));
}
actionCache[fullViewName] = null;
}
}
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.TemplateHelpers_NoTemplate,
viewData.ModelMetadata.RealModelType.FullName
)
);
}
ここでのGetActionCacheの方法は、現在のcontextから簡単である.Itemsで辞書データを取得し、ない場合はインスタンス化してcontextに加える.Itemsにあります.GetDefaultActionsの方法も簡単ですinternal static Dictionary> GetDefaultActions(DataBoundControlMode mode) {
return mode == DataBoundControlMode.ReadOnly ? defaultDisplayActions : defaultEditorActions;
}
GetView Nameは、viewの名前を取得する方法の1つです.
internal static IEnumerable GetViewNames(ModelMetadata metadata, params string[] templateHints) {
foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s))) {
yield return templateHint;
}
// We don't want to search for Nullable, we want to search for T (which should handle both T and Nullable)
Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType;
// TODO: Make better string names for generic types
yield return fieldType.Name;
if (!metadata.IsComplexType) {
yield return "String";
}
else if (fieldType.IsInterface) {
if (typeof(IEnumerable).IsAssignableFrom(fieldType)) {
yield return "Collection";
}
yield return "Object";
}
else {
bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
while (true) {
fieldType = fieldType.BaseType;
if (fieldType == null)
break;
if (isEnumerable && fieldType == typeof(Object)) {
yield return "Collection";
}
yield return fieldType.Name;
}
}
}
デフォルト戻りパラメータ順序はtemplateHintsのviewであり、次にパラメータデータ型に基づいて対応するデフォルトviewを返す.ここで戻ってきたのはStringです.ではExecuteTemplateメソッドに戻ります
string fullViewName = modeViewPath + "/"+ viewName;これは私たちのビューが見つかりました.もし私たちが以前actionCacheにキーを含んでいたら、直接ビューを実行して戻り、次にView Enginesを通じて戻ります.Engines.FindPartialViewがビューを探しに来て、見つかったらビューを出力して返します.そうでなければデフォルトの処理を呼び出す
if (defaultActions.TryGetValue(viewName, out defaultAction)) { actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction }; return defaultAction(MakeHtmlHelper(html, viewData)); }
ここでのdefaultAction対応はDefaultDisplayTemplates.StringTemplateは、私のviewNameがStringなので、ここでのMakeHtmlHelperメソッドは、現在のViewContextとviewDataに基づいて新しいHtmlHelperをインスタンス化します.
DefaultEditorTemplates.StringTemplateメソッドは非常に簡単です.
return html.TextBox(String.Empty, html.ViewContext.ViewData.TemplateInfo.FormattedModelValue, CreateHtmlAttributes("text-box single-line")).ToHtmlString();
主にTextBoxメソッドを呼び出します.
public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary htmlAttributes) { return InputHelper(htmlHelper, InputType.Text, null, name, value, (value == null)/* useViewData */, false/* isChecked */, true/* setId */, true/* isExplicitValue */, htmlAttributes); }
private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, IDictionary htmlAttributes) {
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (String.IsNullOrEmpty(fullName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
}
TagBuilder tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType));
tagBuilder.MergeAttribute("name", fullName, true);
string valueParameter = Convert.ToString(value, CultureInfo.CurrentCulture);
bool usedModelState = false;
switch (inputType) {
case InputType.CheckBox:
bool? modelStateWasChecked = htmlHelper.GetModelStateValue(fullName, typeof(bool)) as bool?;
if (modelStateWasChecked.HasValue) {
isChecked = modelStateWasChecked.Value;
usedModelState = true;
}
goto case InputType.Radio;
case InputType.Radio:
if (!usedModelState) {
string modelStateValue = htmlHelper.GetModelStateValue(fullName, typeof(string)) as string;
if (modelStateValue != null) {
isChecked = String.Equals(modelStateValue, valueParameter, StringComparison.Ordinal);
usedModelState = true;
}
}
if (!usedModelState && useViewData) {
isChecked = htmlHelper.EvalBoolean(fullName);
}
if (isChecked) {
tagBuilder.MergeAttribute("checked", "checked");
}
tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue);
break;
case InputType.Password:
if (value != null) {
tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue);
}
break;
default:
string attemptedValue = (string)htmlHelper.GetModelStateValue(fullName, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName) : valueParameter), isExplicitValue);
break;
}
if (setId) {
tagBuilder.GenerateId(fullName);
}
// If there are any errors for a named field, we add the css attribute.
ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState)) {
if (modelState.Errors.Count > 0) {
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
}
tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
if (inputType == InputType.CheckBox) {
// Render an additional for checkboxes. This
// addresses scenarios where unchecked checkboxes are not sent in the request.
// Sending a hidden input makes it possible to know that the checkbox was present
// on the page when the request was submitted.
StringBuilder inputItemBuilder = new StringBuilder();
inputItemBuilder.Append(tagBuilder.ToString(TagRenderMode.SelfClosing));
TagBuilder hiddenInput = new TagBuilder("input");
hiddenInput.MergeAttribute("type", HtmlHelper.GetInputTypeString(InputType.Hidden));
hiddenInput.MergeAttribute("name", fullName);
hiddenInput.MergeAttribute("value", "false");
inputItemBuilder.Append(hiddenInput.ToString(TagRenderMode.SelfClosing));
return MvcHtmlString.Create(inputItemBuilder.ToString());
}
return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing);
}
ここのInputHelperはhtml文字列を本当に生成する場所です.この方法は全体的にわかりやすいですが、この方法にはこうあることに注意してください.
tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
これは、図のようなオブジェクト上の検証プロパティを処理することです.
皆さんはここまでEditorForの方法について知っていると思いますが、複雑な感じがします.