Get a model's property value from within a custom attribute
I have a custom attribute called FileLink
that uses DisplayForModel()
to generate an ActionLink
to a controller with an ID value.
The attribute looks like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class FileLinkAttribute : Attribute
{
public string ActionName { get; set; }
public string ControllerName { get; set; }
public string IdPropertyName { get; set; }
public FileLinkAttribute(string actionName, string controllerName, string idPropertyName)
{
ActionName = actionName;
ControllerName = controllerName;
IdPropertyName = idName;
}
}
Is it used in a viewmodel, like so:
public class MyViewModel
{
[HiddenInput(DisplayValue = false)]
public int? Id { get; set; }
[HiddenInput(DisplayValue = false)]
public int? TargetId { get; set; }
[FileLink("SomeAction", "SomeController", "TargetId")]
public string Name { get; set; }
}
I have a custom MetadataProvider
derived from DataAnnotationsModelMetadataProvider
that puts the attribute values into the metadata AdditionalValues as so:
// retrieve the values to generate a file link
var fileLinkAttributes = attributes.OfType<FileLinkAttribute>();
if (fileLinkAttributes.Any())
{
var fileLinkAttribute = fileLinkAttributes.First();
metadata.AdditionalValues["FileLinkAttribute"] = fileLinkAttribute;
metadata.TemplateHint = "FileLink";
}
The goal is to have DisplayForModel
call the DisplayTemplate
below to generate a link with the value in TargetId
from MyViewModel
. So if TargetId
is 5, the link will be "SomeController/SomeAction/5"
// This DisplayTemplate for use with the "FileLinkAttribute"
//
// Usage: [FileLi开发者_JAVA技巧nkAttribute("ActionName","ControllerName","IdPropertyName")]
var fileLinkAttribute = (FileLinkAttribute)ViewData.ModelMetadata.AdditionalValues["FileLinkAttribute"];
var targetId = *<GET VALUE OF CONTAINER MODEL IdPropertyName>*;
@Html.ActionLink((string)ViewData.Model, fileLinkAttribute.ActionName, fileLinkAttribute.ControllerName, new { Id = targetId}, null)
With all this in place, I am unable to access the containing object to pull the property value from TargetId. I have tried to do this within the Attribute, within the Provider, and within the DisplayTemplate, with no luck.
Is there a way or place I can access this value to accomplish my intent to read a property value from the viewmodel object containing the attribute?
After reading this post describing the purpose of modelAccessor
in a custom DataAnnotationsModelMetadataProvider:
What is the "Func<object> modelAccessor" parameter for in MVC's DataAnnotationsModelMetadataProvider?
I was able to piece together this (very hackish) solution implemented in the custom provider:
// retrieve the values to generate a file link
var fileLinkAttributes = attributes.OfType<FileLinkAttribute>();
if (fileLinkAttributes.Any())
{
var fileLinkAttribute = fileLinkAttributes.First();
// modelAccessor contains a pointer the container, but the type is not correct but contains the name of the correct type
if (modelAccessor.Target.GetType().ToString().Contains("System.Web.Mvc.AssociatedMetadataProvider"))
{
// get the container model for this metadata
FieldInfo containerInfo = modelAccessor.Target.GetType().GetField("container");
var containerObject = containerInfo.GetValue(modelAccessor.Target);
// get the value of the requested property
PropertyInfo pi = containerObject.GetType().GetProperty(fileLinkAttribute.IdPropertyName);
string idPropertyValue = pi.GetValue(containerObject, null).ToString();
fileLinkAttribute.IdPropertyValue = idPropertyValue;
}
metadata.AdditionalValues["FileLinkAttribute"] = fileLinkAttribute;
metadata.TemplateHint = "FileLink";
}
I am not sure how to derefence a Func to get it's actual target, and the type name comes out all munged wierdly in this example, such as
"System.Web.Mvc.AssociatedMetadataProvider+<>c__DisplayClassb"
So I convert to a string to check if it contains "System.Web.Mvc.AssociatedMetadataProvider". Probably not the best thing but this is the only way I could get it to work.
Oh, I also added a 4th property to the attribute to contain the value:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class FileLinkAttribute : Attribute { public string ActionName { get; set; } public string ControllerName { get; set; } public string IdPropertyName { get; set; } public string IdPropertyValue { get; set; }
public FileLinkAttribute(string actionName, string controllerName, string idPropertyName)
{
ActionName = actionName;
ControllerName = controllerName;
IdPropertyName = idPropertyName;
}
}
and in the DisplayTemplate I do:
var modelId = fileLinkAttribute.IdPropertyValue;
This should be an editor template for strsing or object as that'swhat you are applying the attribute on. So inside ~/Views/Shared/EditorTemplates/string.cshtml
:
@model string
@{
var fileLinkAttribute = (FileLinkAttribute)ViewData.ModelMetadata.AdditionalValues["FileLinkAttribute"];
var targetId = fileLinkAttribute.IdPropertyName;
}
@Html.ActionLink(Model, fileLinkAttribute.ActionName, fileLinkAttribute.ControllerName, new { Id = targetId}, null)
and in your main view:
@Html.EditorFor(x => x.Name)
精彩评论