Using subclass UI hints in a superclass partial view - c#

So I've got a partial view that displays a model containing some info for a person at an organization. One of the properties is a list of titles which has a UIHint attribute on it to determine what display template to use for it.
Lets say that the model looks like this:
public class Info
{
[UIHint("Titles")]
[DataType("Titles")]
public virtual IEnumerable<string> Titles { get; set; }
}
Let's say that the template for Info looks like:
#model Info
#Html.DisplayFor(x=> x.Titles)
Now we have a very specific type of person-at-org instance that we want to display using the same template but we want to use a different display template for the Titles property so we create a subclass of a Info model:
public class SpecificInfo : Info
{
[UIHint("SpecificTitles")]
[DataType("SpecificTitles")]
public override IEnumerable<string> Titles { get; set; }
}
But it's still trying to use the "Titles" display template presumably because the expression passed into the DisplayFor helper thinks that it's accessing the property on the Info class.
Is there any way to get that helper to use the correct display template? I've been thinking that a possible solution would be to create my own DisplayFor extension method that figures out what the runtime type of the model is and uses reflection to find the property and check to see if we are specifying a template there but I can't shake the feeling that there might be an easier way to do it.

You are right setting #model Info in your view makes DisplayeFor use htmlHelper<Info>. It has only acces to Info attributes. You can spcify in view which template to use:
#Html.DisplayFor(x=> x.Titles, Model is SpecificInfo ? "SpecificInfo" : "Info")
but then there is no reason for UIHintAttribute.
As you write you can also write custom DisplayFor method and you can use htmlHelper.ViewData.Model to get actual model attribute(as you suggested in comment ;)):
public static MvcHtmlString CustomDisplayFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
MemberExpression memberExpression = (MemberExpression)expression.Body;
var propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string)null;
var prop = htmlHelper.ViewData.Model.GetType().GetProperty(propertyName);
var customAttributeData = prop.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType.Name == "UIHintAttribute");
if (customAttributeData != null)
{
var templateName = customAttributeData.ConstructorArguments.First().Value as string;
return DisplayExtensions.DisplayFor<TModel, TValue>(htmlHelper, expression, templateName);
}
return DisplayExtensions.DisplayFor<TModel, TValue>(htmlHelper, expression);
}
View:
#Html.CustomDisplayFor(x => x.Titles)

Related

How can I get the metadata of the items in a collection in razor view-engine?

I have a project written in C# on the top on ASP.NET MVC 5 framework. I am trying to decouple my views from my view model so I can make my views reusable. With the heavy use of EditorTemplates I am able to create all of my standard views (i.e create, edit and details) by evaluating the ModelMetadata and the data-annotation-attributes for each property on the model, then render the page. The only thing that I am puzzled with is how to render the Index view.
My index view typically accepts an IEnumerable<object> or IPagedList<object> collection. In my view, I want to be able to evaluate the ModelMetadata of a each object/record in the collection to determine if a property on the object should be displayed or not.
In another words my view-model will look something like this
public class DisplayPersonViewModel
{
public int Id{ get; set; }
[ShowOnIndexView]
public string FirstName { get; set; }
[ShowOnIndexView]
public string LastName { get; set; }
[ShowOnIndexView]
public int? Age { get; set; }
public string Gender { get; set; }
}
Then my Index.cshtml view will accepts IPagedList<DisplayPersonViewModel> for each record in the collection, I want to display the value of the property that is decorated with ShowOnIndexView attribute.
Typically I would be able to do that my evaluating the ModelMetadata in my view with something like this
#model IPagedList<object>
#{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata))
.OrderBy(x => x.Order)
.ToList();
}
<tr>
#foreach(var element in elements)
{
var onIndex = element.ContainerType.GetProperty(element.PropertyName)
.GetCustomAttributes(typeof(ShowOnIndexView), true)
.Select(x => x as ShowOnIndexView)
.FirstOrDefault(x => x != null);
if(onIndex == null)
{
continue;
}
#Html.Editor(element.PropertyName, "ReadOnly")
}
</tr>
Then my controller will look something like this
public class PersonController : Controller
{
public ActionResult Index()
{
// This would be a call to a service to give me a collection with items. but I am but showing the I would pass a collection to my view
var viewModel = new List<DisplayPersonViewModel>();
return View(viewModel);
}
}
However the problem with evaluating ModelMetadata for the IPagedList<DisplayPersonViewModel> is that it gives me information about the collection itself not about the generic type or the single model in the collection. In another words, I get info like, total-pages, items-per-page, total-records....
Question
How can I access the ModelMetadata info for each row in the collection to be able to know which property to display and which not to?
I will preface this answer by recommending you do not pollute your view with this type of code. A far better solution would be to create a custom HtmlHelper extension method to generate the html, which gives you far more flexibility, can be unit tested, and is reusable.
The first thing you will need to change is the model declaration in the view which needs to be
#model object
otherwise you will throw this exception (List<DisplayPersonViewModel> is not IEnumerable<object> or IPagedList<object>, but it is object)
Note that it is not clear if you want the ModelMetadata for the type in the collection or for each item in the collection, so I have included both, plus code that gets the type
#model object
#{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata)).OrderBy(x => x.Order).ToList();
// Get the metadata for the model
var collectionMetaData = ViewData.ModelMetadata;
// Get the collection type
Type type = collectionMetaData.Model.GetType();
// Validate its a collection and get the type in the collection
if (type.IsGenericType)
{
type = type.GetInterfaces().Where(t => t.IsGenericType)
.Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Single().GetGenericArguments()[0];
}
else if (type.IsArray)
{
type = type.GetElementType();
}
else
{
// its not a valid model
}
// Get the metadata for the type in the collection
ModelMetadata typeMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, type);
}
....
#foreach (var element in collectionMetaData.Model as IEnumerable))
{
// Get the metadata for the element
ModelMetadata elementMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => element, type);
....
Note that using reflection to determine if the attribute exists in each iteration of your loop is inefficient, and I suggest you do that once (based on the typeMetadata). You could then (for example) generate an array of bool values, and then use an indexer in the loop to check the value). A better alternative would be to have your ShowOnIndexView attribute implement IMetadataAware and add a value to the AdditionalValues of ModelMetadata so that var onIndex code is not required at all. For an example of implementing IMetadataAware, refer CustomAttribute reflects html attribute MVC5

Custom DisplayName

Currently I am trying to set dynamic the DisplayName of a property but I cannot find a way how get information of current property inside an attribute.
This is what I want to achieve:
Desired Outcome
My Attribute
public class DisplayNameFromPropertyAttribute : DisplayNameAttribute
{
public DisplayNameFromPropertyAttribute(string propertyName)
: base(GetDisplayName(propertyName))
{
}
private string GetDisplayBame(string propertyName)
{
// Get the value from the given property
}
}
My Model
I am trying to read the value from MyDisplayName into my custom DisplayNameFromProperty attribute
public class MyAwesomeModel
{
public string MyDisplayName { get; set; }
[Required]
[DisplayNameFromProperty("MyDisplayName")]
public string MyValue { get; set; }
}
My Page
#Html.LabelFor(model => model.MyValue)
#Html.TextBoxFor(model => model.MyValue)
#Html.ValidationMessageFor(model => model.MyValue)
Question
Currently I cannot find any reference on internet doing the same. Can someone help me out?
If this is not possible: What are the alternatives to achieve same result?
The ComponentModel.DataAnnotations validation attributes should use my custom display name
The #Html.LabelFor(model => model.MyValue) should use my custom display name
You can create a cusom HTML extension method that lets you do #Html.DictionaryLabelFor(x=>x.Property) and pull it from a dictionary
public static IHtmlString DictionaryLabelFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression, string text = null, string prefix = null)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var displayName = metadata.DisplayName;
if (string.IsNullOrWhiteSpace(text))
{
// Your code to get the label via reflection
// of the object
string labelText = "";
return html.Label(prefix + metadata.PropertyName, labelText);
}
else
{
return html.Label(prefix + metadata.PropertyName, text);
}
}
Overriding the properties on this works, only thing missing is the custom html attributes which wasn't needed when I wrote it
The validation error message is slightly different, you should always write custom errors for these fields so you can rely on them in a resx, you look at the modelstate for the (prefix + key) to get the errors, then get the translated value for each case.
You're best avoiding overwriting the standard HTML call as you'll be making excess calls where not needed elsewhere.
When you have that working and understand it the error messages section is pretty trivial to write in yourself, depends how you want to do the formatting on errors. I'm not writing it in as it's basically doing everything for you and if you don't understand how it works you'll be SOL when it comes to writing further extensions

Expression<Func<TModel, string>> where TModel is an interface, type is never the passed implemenation

I created a custom HtmlHelper to render an address on a webpage. The address needs to be dynamic based on the country the address is in. For this I have a simple interface with multiple implementations. My controller passes the specific implementation to the view which is strongly typed to the interface. When debugging, the view shows the correct implementing type and I pass that model to my custom HtmlHelper as a generic type parameter. This custom HtmlHelper only accepts models of type IAddress or its implementation, but the TModel is always IAddress no matter what type is passed into it. This is an issue due to the attributes on the individual implementations as I use the existing HtmlHelpers such as Html.EditorFor and Html.LabelFor within my own helper to render the fields and labels for the address including proper validation when submitted.
I believe the issues is that the View is strongly typed to IAddress and the HtmlHelper ignores the actual model type and goes straight for the view type, how can I get around this?
Interface and Implementing Classes:
public interface IAddress
{
// Dumbed down to one property to save space
String City { get; set; }
String State { get; set; }
}
public class AddressUS : IAddress
{
// Required and displayed on page.
[DisplayName("City")]
[Required(ErrorMessage = "City is required!")]
public String City { get; set; }
// Required and displayed on page.
[DisplayName("State")]
[Required(ErrorMessage = "State is required!")]
public String State { get; set; }
}
public class AddressJP : IAddress
{
// Required and displayed on page.
[DisplayName("Prefecture")]
[Required(ErrorMessage = "Prefecture is required!")]
public String City { get; set; }
// Not required and not displayed on page.
[DisplayName("State")]
public String State { get; set; }
}
Controller Action:
public ActionResult DisplayAddress()
{
// Returning a specific type for testing.
AddressJP address = new AddressJP() { City = "Test" };
}
View:
#*
Set to the interface to accept all implementations, but possibly also the cause of the issue.
Setting to AddressJP, but I need it dynamic.
*#
#model IAddress
#using Custom.HtmlExtensions
<div id="address">
#Html.AddressEditorForModel(true)
</div>
HtmlHelper:
public static MvcHtmlString AddressEditorForModel<TModel>(this HtmlHelper<TModel> helper, Boolean showLabels) where TModel : IAddress
{
// TModel is always IAddress unless I change the view to be a specific implementation.
StringBuilder sb = new StringBuilder();
PropertyInfo pInfo = typeof(TModel).GetProperty("City");
ParameterExpression paramExpr = Expression.Parameter(typeof(TModel));
MemberExpression propertyAccess = Expression.MakeMemberAccess(paramExpr, pInfo);
var lambdaExpr = Expression.Lambda<Func<TModel, string>>(propertyAccess, paramExpr);
if (showLabels)
sb.AppendLine(helper.LabelFor(lambdaExpr).ToString());
sb.AppendLine(helper.EditorFor(lambdaExpr).ToString());
return MvcHtmlString.Create(sb.ToString());
}
You don't have to use reflection to get to the value. You've specified a generic type constraint, so .NET knows the TModel has to be IAddress.
You should simply be able to do
var city = helper.ViewData.Model.City;
In fact, you should be able to do away with the generic method altogether - just use HtmlHelper<IAddress> as the parameter, and it should work just fine.
Also, you can use helper.Label etc. instead of helper.LabelFor - they work the same way but don't need an expression, just the name of the property.

DataType mvc empty

I have a Model with properties like this:
[DataType(DataType.MultilineText)]
[MaxLength(512)]
public string Description
{
get
{
this.OnReadingDescription(ref _description);
return _description;
}
set
{
this.OnDescriptionChanging(ref value);
this._description = value;
this.OnDescriptionChanged();
}
}
and a View like this:
#using AspMvcBase.Bootstrap
#model NpoDb.Presentation.Web.Models.Media
#Html.EditForm();
EditForm is a custom HtmlHelper extension which renders every property in the ViewModel.
This part works realy fine but I want do render some of the properties different so I added the [DataType(DataType.MultilineText)] Attribute.
I want to test now in my HtmlHelper if there is an Attribute for a MultilineText.
modelMetadata.DataTypeName.Equals(DataType.EmailAddress.ToString())
But the problem is that DataTypeName is null. Even if I look at it in the View it's null.
This is one of the better resources I found about this.Brad Wilson MVC2 Template
But I can't figure out why it doesn't work in my case.
So the basic question is: How can I access Attributes in a HtmlHelper?
I would really appreciate some help. :)
If you are trying to get the attribute for a property of your model, you can do this:
public static string EditForm(this HtmlHelper helper)
{
var model = helper.ViewData.Model;
var dataType = model
.GetType()
.GetProperty("Description")
.GetCustomAttribute<DataTypeAttribute>()
.DataType;
...
}

How can I make Html.CheckBoxFor() work on a string field?

I'm using ASP.NET MVC3 with Razor and C#. I am making a form builder of sorts, so I have a model that has a collection of the following object:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public MyFormType Type { get; set; }
}
MyFormType is just an enum that tells me if the form field is a checkbox, or textbox, or file upload, or whatever. My editor template looks something like this (see the comment):
~/Views/EditorTemplates/MyFormField.cshtml
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.Value) // This does not work!
}
}
I tried casting/converting the m.Value to a bool in the lambda expression for CheckBoxFor(), but that threw an error. I would just manually construct a checkbox input, but CheckBoxFor() seems to do two things that I can't seem to replicate:
Creates a hidden input that somehow gets populated by the checkbox. This appears to be what the model binder picks up.
Generates the name form the object so that the model binder gets the value into the right property.
Does anyone know a way around using CheckBoxFor() on a string, or a way to replicate its functionality manually, so that I can make this work?
You could also add a property on your viewmodel:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public bool CheckBoxValue
{
get { return Boolean.Parse(Value); }
}
public MyFormType Type { get; set; }
}
Your view would be something like this:
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.CheckBoxValue) // This does work!
}
}
Use Boolean.TryParse if you want to avoid exceptions.
One way is to create your own htmlhelper extension method.
public static MvcHtmlString CheckBoxStringFor<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, string>> expression)
{
// get the name of the property
string[] propertyNameParts = expression.Body.ToString().Split('.');
string propertyName = propertyNameParts.Last();
// get the value of the property
Func<TModel, string> compiled = expression.Compile();
string booleanStr = compiled(html.ViewData.Model);
// convert it to a boolean
bool isChecked = false;
Boolean.TryParse(booleanStr, out isChecked);
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("id", propertyName);
checkbox.MergeAttribute("name", propertyName);
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("value", "true");
if (isChecked)
checkbox.MergeAttribute("checked", "checked");
TagBuilder hidden = new TagBuilder("input");
hidden.MergeAttribute("name", propertyName);
hidden.MergeAttribute("type", "hidden");
hidden.MergeAttribute("value", "false");
return MvcHtmlString.Create(checkbox.ToString(TagRenderMode.SelfClosing) + hidden.ToString(TagRenderMode.SelfClosing));
}
The usage is the same as CheckBoxFor helper (e.Value is a string)
#Html.CheckBoxStringFor(e => e.Value)
Use the Checkbox, this simple way works fine
#Html.CheckBox("IsActive", Model.MyString == "Y" ? true : false)
I had this problem as well but was unable to modify the view model. Tried mdm20s solution but as i suspected it does not work on collection properties (it does not add the indexes to the names and ids like the native html helpers). To overcome this you can use the Html.CheckBox instead. It adds the proper indexes and you can pass the value of the checkbox yourself.
If you really want to use an expression you can always write a wrapper similar to mdm20s but replace everything after the TryParse with
return Html.CheckBox("propertyName", isChecked). Obviously you will need to add using System.Web.Mvc.Html as well.

Categories

Resources