I am setting custom attributes to secure objects within a view. Here is the definition for my custom attributes:
[AttributeUsage(AttributeTargets.Property)]
public class SecureObjectAttribute : Attribute, IMetadataAware
{
public void OnMetadataCreated(ModelMetadata metadata)
{
if(!metadata.AdditionalValues.ContainsKey("isSecure"))
{
if (ObjectId == 1)
{
metadata.AdditionalValues.Add("isSecure", true);
}
else
{
metadata.AdditionalValues.Add("isSecure", false);
}
}
}
public int ObjectId { get; set; }
}
Here is my View Model:
public class HomeViewModel
{
[SecureObject(ObjectId = 1)]
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Address")]
public string Address { get; set; }
}
I am defining helpers for Secure Labels and Secure TextBoxes. Here are my helper functions:
public static MvcHtmlString SecureLabelFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if ((bool)metadata.AdditionalValues["isSecure"])
{
return null;
}
else
{
return html.LabelFor(expression);
}
}
public static MvcHtmlString SecureTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if ((bool)metadata.AdditionalValues["isSecure"])
{
return null;
}
else
{
return html.TextBoxFor(expression);
}
}
All of this works correctly, however, the problem that I am having is that every time I define a secure label and a secure textbox, the OnMetadataCreated method is invoked.
I am making a trip to the database on the OnMetadataCreated method (not shown in the example) to get permissions for the object, and I want to avoid duplicate trips for a single object.
The following is an example of code that is causing the OnMetadataCreated to be invoked twice in my View:
#Html.SecureLabelFor(m => m.Name)
#Html.SecureTextBoxFor(m => m.Name)
Any suggestions on how I can avoid a second call to the OnMetadataCreated method, or avoid a duplicate trip to the database?
You could store the result of the execution of the query in the HttpContext to avoid roundtrips to the database within the same request:
public void OnMetadataCreated(ModelMetadata metadata)
{
// TODO: if you are using a DI framework you could pass the context
// as a constructor argument dependency and use HttpContextBase
HttpContext context = HttpContext.Current;
bool isSecure;
string key = "isSecure_" + ObjectId;
if (context.Items.Contains(key))
{
// The isSecure value was found in the HttpContext =>
// no need to query the database once again within this request
isSecure = (bool)context.Items[key];
}
else
{
// Get the value from the database
isSecure = GetFromDb(context.User.Identity.Name, ObjectId);
// and store into the HttpContext to avoid roundtrips to the database
// within this request
context.Items[key] = isSecure;
}
...
}
Related
I have an MVC app that uses dynamic business objects that are inherited from a parent object type. For example the base class Client might have two sub classes called Vendor and ServiceProvider, and these are all handled by the same controller. I have a partial view that I load on the right side of the page when viewing the client's details called _Aside.cshtml. When I load the client I try to look for a specific Aside first and failing that I load a generic one. Below is what the code looks like.
#try
{
#Html.Partial("_" + Model.Type.TypeName + "Aside")
}
catch (InvalidOperationException ex)
{
#Html.Partial("_Aside")
}
The TypeName property would have the word "Vendor" or "ServiceProvider" in it.
Now this works fine but the problem is I only want it to fail over if the view is not found, It's also failing over when there is an actual InvalidOperationException thrown by the partial view (usually the result of a child action it might call). I've thought about checking against Exception.Message but that seems a bit hackish. Is there some other way I can get the desired result without having to check the Message property or is that my only option at this point?
ex.Message = "The partial view '_ServiceProviderAside' was not found or no view
engine supports the searched locations. The following locations were
searched: (... etc)"
UPDATE: This is the class with extension methods I have currently in my project based off of Jack's answer, and Chao's suggestions as well.
//For ASP.NET MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
return view.View != null;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : HtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return OptionalPartial(helper, viewName, fallbackViewName, null);
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, object model)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName, model) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName, model);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
}
UPDATE: If you happen to be using ASP.NET Core MVC, swap the PartialExists() methods for these three methods, and change all of the usages of HtmlHelper for IHtmlHelper in the other methods. Skip this if you're not using ASP.NET Core
//For ASP.NET Core MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this IHtmlHelper helper, string viewName)
{
var viewEngine = helper.ViewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(helper.ViewContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
var viewEngine = controllerContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(controllerContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ViewContext viewContext, string viewName)
{
var viewEngine = viewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(viewContext, viewName, false);
return view.View != null;
}
}
In my view...
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside")
//or
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside", Model.AsideViewModel)
Came across this answer while trying to solve the problem of nested sections as I wanted to include styles and scripts in an intermediate view. I ended up deciding the simplest approach was convention of templatename_scripts and templatename_styles.
So just to add to the various options here is what I'm using based on this.
public static class OptionalPartialExtensions
{
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName, "View name cannot be empty");
}
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
}
This brings the my most common use cases in to the extension methods helping keep the views that bit cleaner, the RenderPartials were added for completeness.
I had a similar requirement. I wanted to keep the view markup cleaner and also to avoid generating the dynamic view name twice. This is what I came up with (modified to match your example):
Helper extension:
public static string FindPartial(this HtmlHelper html, string typeName)
{
// If you wanted to keep it in the view, you could move this concatenation out:
string viewName = "_" + typeName + "Aside";
ViewEngineResult result = ViewEngines.Engines.FindPartialView(html.ViewContext, viewName);
if (result.View != null)
return viewName;
return "_Aside";
}
View:
#Html.Partial(Html.FindPartial(Model.Type.TypeName))
or with access to the Model within the partial :
#Html.Partial(Html.FindPartial(Model.Type.TypeName), Model)
You could try the FindPartialView method to check if the view exists. Something along these lines might work (untested):
public bool DoesViewExist(string name)
{
string viewName = "_" + Model.Type.TypeName + "Aside";
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName , null);
return (viewResult.View != null);
}
Info on the FindPartialView method for ASP MVC 3
Bug fix to handle null viewName or null fallbackViewName (replace appropriate code in OP):
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
string partialToRender = null;
if (viewName != null && PartialExists(helper, viewName))
{
partialToRender = viewName;
}
else if (fallbackViewName != null && PartialExists(helper, fallbackViewName))
{
partialToRender = fallbackViewName;
}
if (partialToRender != null)
{
return helper.Partial(partialToRender, model);
}
else
{
return MvcHtmlString.Empty;
}
}
I have edited the OP's code (which combines code from multiple answers), but my edit is pending peer review.
If a Get Action returns a View with a "Car" model. The view displays info from the object and takes input to post within a form to another action that takes an object of type "Payment"
The Model on the view is of type Car and gives me stronglytyped html support and some other features like displaytext. But for posting I there is no Htmlhelper support like TextBox(x => x.amount I need to make it like #Html.TextBox("Amount"...
Its possible, but is this the only option?
You can do this:
#{
var paymentHtml = Html.HtmlHelperFor<Payment>();
}
#paymentHtml.EditorFor(p => p.Amount)
with this extension method:
public static class HtmlHelperFactoryExtensions {
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper) {
return HtmlHelperFor(htmlHelper, default(TModel));
}
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper, TModel model) {
return HtmlHelperFor(htmlHelper, model, null);
}
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper, TModel model, string htmlFieldPrefix) {
var viewDataContainer = CreateViewDataContainer(htmlHelper.ViewData, model);
TemplateInfo templateInfo = viewDataContainer.ViewData.TemplateInfo;
if (!String.IsNullOrEmpty(htmlFieldPrefix))
templateInfo.HtmlFieldPrefix = templateInfo.GetFullHtmlFieldName(htmlFieldPrefix);
ViewContext viewContext = htmlHelper.ViewContext;
ViewContext newViewContext = new ViewContext(viewContext.Controller.ControllerContext, viewContext.View, viewDataContainer.ViewData, viewContext.TempData, viewContext.Writer);
return new HtmlHelper<TModel>(newViewContext, viewDataContainer, htmlHelper.RouteCollection);
}
static IViewDataContainer CreateViewDataContainer(ViewDataDictionary viewData, object model) {
var newViewData = new ViewDataDictionary(viewData) {
Model = model
};
newViewData.TemplateInfo = new TemplateInfo {
HtmlFieldPrefix = newViewData.TemplateInfo.HtmlFieldPrefix
};
return new ViewDataContainer {
ViewData = newViewData
};
}
class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
}
}
If I understand your question correctly, here's some code I just wrote for one of my projects to do something similar. It doesn't require anything special like what was suggested by Max Toro.
#{
var teamHelper = new HtmlHelper<Team>(ViewContext, this);
}
#using (teamHelper.BeginForm())
{
#teamHelper.LabelFor(p => p.Name)
#teamHelper.EditorFor(p => p.Name)
}
Adding to the implementation by Max Toro, here are a couple more for when you have a non-null model but don't have static type information (these two methods need to be embedded into the implementation Max provides).
These methods work well when you have dynamically retrieved property names for a model and need to call the non-generic HtmlHelper methods that take a name instead of an expression:
#Html.TextBox(propertyName)
for example.
public static HtmlHelper HtmlHelperFor( this HtmlHelper htmlHelper, object model )
{
return HtmlHelperFor( htmlHelper, model, null );
}
public static HtmlHelper HtmlHelperFor( this HtmlHelper htmlHelper, object model, string htmlFieldPrefix )
{
var t = model.GetType();
var viewDataContainer = CreateViewDataContainer( htmlHelper.ViewData, model );
TemplateInfo templateInfo = viewDataContainer.ViewData.TemplateInfo;
if( !String.IsNullOrEmpty( htmlFieldPrefix ) )
templateInfo.HtmlFieldPrefix = templateInfo.GetFullHtmlFieldName( htmlFieldPrefix );
ViewContext viewContext = htmlHelper.ViewContext;
ViewContext newViewContext = new ViewContext( viewContext.Controller.ControllerContext, viewContext.View, viewDataContainer.ViewData, viewContext.TempData, viewContext.Writer );
var gt = typeof( HtmlHelper<> ).MakeGenericType( t );
return Activator.CreateInstance( gt, newViewContext, viewDataContainer, htmlHelper.RouteCollection ) as HtmlHelper;
}
For ASP.NET Core 2
public static class HtmlHelperFactoryExtensions
{
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper)
{
return HtmlHelperFor(htmlHelper, default(TModel));
}
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper, TModel model)
{
return HtmlHelperFor(htmlHelper, model, null);
}
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper, TModel model, string htmlFieldPrefix)
{
ViewDataDictionary<TModel> newViewData;
var runtimeType = htmlHelper.ViewData.ModelMetadata.ModelType;
if (runtimeType != null && typeof(TModel) != runtimeType && typeof(TModel).IsAssignableFrom(runtimeType))
{
newViewData = new ViewDataDictionary<TModel>(htmlHelper.ViewData, model);
}
else
{
newViewData = new ViewDataDictionary<TModel>(htmlHelper.MetadataProvider, new ModelStateDictionary())
{
Model = model
};
}
if (!String.IsNullOrEmpty(htmlFieldPrefix))
newViewData.TemplateInfo.HtmlFieldPrefix = newViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldPrefix);
ViewContext newViewContext = new ViewContext(htmlHelper.ViewContext, htmlHelper.ViewContext.View, newViewData, htmlHelper.ViewContext.Writer);
var newHtmlHelper = htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper<TModel>>();
((HtmlHelper<TModel>)newHtmlHelper).Contextualize(newViewContext);
return newHtmlHelper;
}
}
If I understand your problem correctly, try:
#Html.EditorFor(x => x.Amount)
You could also create an editor template for Payment. See this page for details on doing this.
If I'm misunderstanding, some sample code might help.
I'm trying to generate an Html.ActionLink with the following viewmodel:
public class SearchModel
{
public string KeyWords {get;set;}
public IList<string> Categories {get;set;}
}
To generate my link I use the following call:
#Html.ActionLink("Index", "Search", Model)
Where Model is an instance of the SearchModel
The link generated is something like this:
http://www.test.com/search/index?keywords=bla&categories=System.Collections.Generic.List
Because it obviously is only calling the ToString method on every property.
What I would like to see generate is this:
http://www.test.com/search/index?keywords=bla&categories=Cat1&categories=Cat2
Is there any way I can achieve this by using Html.ActionLink
In MVC 3 you're just out of luck because the route values are stored in a RouteValueDictionary that as the name implies uses a Dictionary internally which makes it not possible to have multiple values associated to a single key. The route values should probably be stored in a NameValueCollection to support the same behavior as the query string.
However, if you can impose some constraints on the categories names and you're able to support a query string in the format:
http://www.test.com/search/index?keywords=bla&categories=Cat1|Cat2
then you could theoretically plug it into Html.ActionLink since MVC uses TypeDescriptor which in turn is extensible at runtime. The following code is presented to demonstrate it's possible, but I would not recommend it to be used, at least without further refactoring.
Having said that, you would need to start by associating a custom type description provider:
[TypeDescriptionProvider(typeof(SearchModelTypeDescriptionProvider))]
public class SearchModel
{
public string KeyWords { get; set; }
public IList<string> Categories { get; set; }
}
The implementation for the provider and the custom descriptor that overrides the property descriptor for the Categories property:
class SearchModelTypeDescriptionProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(
Type objectType, object instance)
{
var searchModel = instance as SearchModel;
if (searchModel != null)
{
var properties = new List<PropertyDescriptor>();
properties.Add(TypeDescriptor.CreateProperty(
objectType, "KeyWords", typeof(string)));
properties.Add(new ListPropertyDescriptor("Categories"));
return new SearchModelTypeDescriptor(properties.ToArray());
}
return base.GetTypeDescriptor(objectType, instance);
}
}
class SearchModelTypeDescriptor : CustomTypeDescriptor
{
public SearchModelTypeDescriptor(PropertyDescriptor[] properties)
{
this.Properties = properties;
}
public PropertyDescriptor[] Properties { get; set; }
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(this.Properties);
}
}
Then we would need the custom property descriptor to be able to return a custom value in GetValue which is called internally by MVC:
class ListPropertyDescriptor : PropertyDescriptor
{
public ListPropertyDescriptor(string name)
: base(name, new Attribute[] { }) { }
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { throw new NotImplementedException(); }
}
public override object GetValue(object component)
{
var property = component.GetType().GetProperty(this.Name);
var list = (IList<string>)property.GetValue(component, null);
return string.Join("|", list);
}
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType
{
get { throw new NotImplementedException(); }
}
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) { }
public override bool ShouldSerializeValue(object component)
{
throw new NotImplementedException();
}
}
And finally to prove that it works a sample application that mimics the MVC route values creation:
static void Main(string[] args)
{
var model = new SearchModel { KeyWords = "overengineering" };
model.Categories = new List<string> { "1", "2", "3" };
var properties = TypeDescriptor.GetProperties(model);
var dictionary = new Dictionary<string, object>();
foreach (PropertyDescriptor p in properties)
{
dictionary.Add(p.Name, p.GetValue(model));
}
// Prints: KeyWords, Categories
Console.WriteLine(string.Join(", ", dictionary.Keys));
// Prints: overengineering, 1|2|3
Console.WriteLine(string.Join(", ", dictionary.Values));
}
Damn, this is probably the longest answer I ever give here at SO.
with linq of course...
string.Join("", Model.Categories.Select(c=>"&categories="+c))
I have a requirement to roll my own BeginLabel helper for Mvc. I checked/stole the concept from the Mvc source for the html.beginForm / ajax.beginForm methods.
public static Label BeginLabel(this HtmlHelper htmlHelper)
{
TagBuilder tagBuilder = new TagBuilder("label");
HttpResponseBase response = htmlHelper.ViewContext.HttpContext.Response;
response.Write(tagBuilder.ToString(TagRenderMode.StartTag));
return new Label(response);
}
The Label simply implements IDisposable interface to enable closing off the label:
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
_httpResponse.Write("</label>");
}
}
Usage looks like this:
#using (Html.BeginLabel())
{
#Html.TextBoxFor(f => f.FirstName)
#Html.ValidationMessageFor(f => f.FirstName)
}
It looks like i'm missing something as the labels always get rendered at the top of the html and while this is obvious to me because i'm writing to the response, I can't see how the native BeginForm() is achieving this. Can anyone shed any light on this?
public class MvcLabel : IDisposable
{
// Fields
private bool _disposed;
private readonly TextWriter _writer;
public MvcLabel(ViewContext viewContext)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
this._writer = viewContext.Writer;
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
this._disposed = true;
this._writer.Write("</label>");
}
}
public void EndLabel()
{
this.Dispose(true);
}
}
and
public static class HtmlHelperExtension
{
// Methods
public static MvcLabel BeginLabel(this HtmlHelper html, string expression)
{
return html.BeginLabel(expression, null);
}
public static MvcLabel BeginLabel(this HtmlHelper html, string expression, string labelText)
{
return LabelHelper(html, ModelMetadata.FromStringExpression(expression, html.ViewData), expression, labelText);
}
public static MvcLabel BeginLabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
return html.BeginLabelFor<TModel, TValue>(expression, null);
}
public static MvcLabel BeginLabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText)
{
return LabelHelper(html, ModelMetadata.FromLambdaExpression<TModel, TValue>(expression, html.ViewData), ExpressionHelper.GetExpressionText(expression), labelText);
}
public static MvcLabel BeginLabelForModel(this HtmlHelper html)
{
return html.BeginLabelForModel(null);
}
public static MvcLabel BeginLabelForModel(this HtmlHelper html, string labelText)
{
return LabelHelper(html, html.ViewData.ModelMetadata, string.Empty, labelText);
}
public static void EndLabel(this HtmlHelper htmlHelper)
{
htmlHelper.ViewContext.Writer.Write("</label>");
}
internal static MvcLabel LabelHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string labelText = null)
{
string str = labelText ?? (metadata.DisplayName ?? (metadata.PropertyName ?? htmlFieldName.Split(new char[] { '.' }).Last<string>()));
TagBuilder tagBuilder = new TagBuilder("label");
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
html.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
if (!string.IsNullOrEmpty(str))
{
tagBuilder = new TagBuilder("span");
tagBuilder.SetInnerText(str);
html.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.Normal));
}
return new MvcLabel(html.ViewContext);
}
}
Hope i can help others...
Is there a quick way to default the error message for all your fields in a model?
I want the validation to return the text:
" * required "
...but dont want to manually set it on each field.
Thanks Paul
you can write your custom Required Attribute
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class AEMRequiredAttribute: ValidationAttribute
{
private const string _defaultErrorMessage = "* required";
public AEMRequiredAttribute()
: base(_defaultErrorMessage)
{ }
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, "* required", name);
}
public override bool IsValid(object value)
{
if (value == null || String.IsNullOrWhiteSpace(value.ToString())) return false;
else return true;
}
}
call this attribute as below :
public partial class AEMClass
{
[DisplayName("Dis1")]
[AEMRequiredAttribute]
public string ContractNo { get; set; }
}
You could create a new HTML helper and then call into the underlying ValidationMessage or ValidationMessageFor helpers setting the message text as you do so.
Something based on ValidationMessageFor would look like this:
public static class HtmlHelperExtensions {
public static IHtmlString ValidatorMessageWithMyTextFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) {
return htmlHelper.ValidationMessageFor<TModel, TProperty>(expression, "required *");
}
}
And you can add that to your view using
#Html.ValidatorMessageWithMyTextFor(m=>m.MyModelPropertyToValidate)
Of course that all works from the view side of the app and not the model side so it all depends where you would like to embed the messages. If it's the model side then AEM's solution is a good one.