Is there any possible way to extend the basic html helpers (TextBoxFor, TextAreaFor, etc) using extension methods on their output, instead of just re-writing the entire methods completely? For instance, adding in ...
#Html.TextBoxFor( model => model.Name ).Identity("idName")
I know I can achieve this using the following, already..
#Html.TextBoxFor( model => model.Name, new { #id = "idName" })
But that gets clunky and frustrating to manage when you have to start adding a lot of properties. Is there any way to add extensions to these inherently without just passing in htmlAttributes for every little detail?
As #AaronShockley says, because TextBoxFor() returns an MvcHtmlString, your only option for developing a 'fluid API' style of amending the output would be to operate on the MvcHtmlStrings returned by the helper methods. A slightly different way of doing this which I think approaches what you're after would be to use a 'property builder' object, like this:
public class MvcInputBuilder
{
public int Id { get; set; }
public string Class { get; set; }
}
...and to set up extension methods like this:
public static MvcHtmlString TextBoxFor<TModel, TProp>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProp>> expression,
params Action<MvcInputBuilder>[] propertySetters)
{
MvcInputBuilder builder = new MvcInputBuilder();
foreach (var propertySetter in propertySetters)
{
propertySetter.Invoke(builder);
}
var properties = new RouteValueDictionary(builder)
.Select(kvp => kvp)
.Where(kvp => kvp.Value != null)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return htmlHelper.TextBoxFor(expression, properties);
}
You can then do stuff like this in your View:
#this.Html.TextBoxFor(
model => model.Name,
p => p.Id = 7,
p => p.Class = "my-class")
This gives you strong typing and intellisense for input properties, which you could customise for each extension method by adding properties to an appropriate MvcInputBuilder subclass.
All of the basic html helpers return an object of type System.Web.Mvc.MvcHtmlString. You can set up extension methods for that class. Here is an example:
public static class MvcHtmlStringExtensions
{
public static MvcHtmlString If(this MvcHtmlString value, bool check)
{
if (check)
{
return value;
}
return null;
}
public static MvcHtmlString Else(this MvcHtmlString value, MvcHtmlString alternate)
{
if (value == null)
{
return alternate;
}
return value;
}
}
Then you can use these in a view like:
#Html.TextBoxFor(model => model.Name)
.If(Model.Name.StartsWith("A"))
.Else(Html.TextBoxFor(model => model.LastName)
To make extension methods that modify attributes on the rendered HTML tag, you'll have to convert the result to a string, and find and replace the value you're looking for.
using System.Text.RegularExpressions;
public static MvcHtmlString Identity(this MvcHtmlString value, string id)
{
string input = value.ToString();
string pattern = #"(?<=\bid=")[^"]*";
string newValue = Regex.Replace(input, pattern, id);
return new MvcHtmlString(newValue);
}
public static MvcHtmlString Name(this MvcHtmlString value, string id)
{
string input = value.ToString();
string pattern = #"(?<=\bname=")[^"]*";
string newValue = Regex.Replace(input, pattern, id);
return new MvcHtmlString(newValue);
}
The id and name attributes are always added by the html helpers, but if you want to work with attributes that may not be there (and you'll have to add them instead of just replacing them), you'll need to modify the code.
Related
I have written an attribute before, but I I have not written a validation attribute before. I am seriously confused about how it all works together. I have read most of the tutorials online about how to go about accomplishing this. But I am left with a couple of questions to ponder.
Keep in mind that I am trying to write a requiredIf attribute that will only call a remote function if a certain Jquery variable is set... which incidentally is a variable that is pulled from view state... I guess I could make that part of my view model. But I digress
1) The C# code is slightly confusing. I know my attribute should extend the ValidationAttribute, IClientValidatable class and interface respectively. But I am a little confused about what each of the overidden methods should be doing? I am trying to write a requiredIf, how does overwriting these methods help me accomplish this goal?
2) If the variable is not there, I simply don't want the remote function to attempt to validate the field. I don't want any message to pop up on my form. Alot of the tutorials seem to revolve around that.
3) I am confused about what I need to do with the jquery to add this function to the view... What do I need to add to the JQuery to get this thing to work... It seems like a lot of extra coding when I could simply just type up a jquery function that did the same thing with just the same ore less coding... I know it also adds server side validation which is good. But still...
Here is what I have for my jquery side of this equation...
(function ($) {
$validator.unobtrusive.adapters.addSingleVal("requiredifattribute", "Dependent");
$validator.addMethod("requiredifattribute", function (value, element, params) {
if (!this.optional(element)) {
var otherProp = $('#' + params)
return (otherProp.val() != value);
}
return true;
})
}(jQuery));
Here is my Attribute (which is basically carbon copied out of one the required if tutorials... I know I need to customize it more, but once I get a better idea of what every piece is doing I will do that...
[AttributeUsage(AttributeTargets.Property)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable {
private const string errorMessage = "The {0} is required.";
//public string
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue){
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var field = validationContext.ObjectInstance.GetType().GetProperty(DependentProperty);
if (field != null) {
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && TargetValue == null) || (dependentValue.Equals(TargetValue))) {
if (!innerAttribute.IsValid(value))
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
ModelClientValidationRule modelClientValidationRule = new ModelClientValidationRule {
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "requiredifattribute"
};
modelClientValidationRule.ValidationParameters.Add("dependent", DependentProperty);
yield return modelClientValidationRule;
}
}
UPDATE: What I have simply isn't working
Here is how a property in my model is anotated with the above attribute
[RequiredIf("isFlagSet", true)]
[Remote("ValidateHosFin", "EditEncounter", AdditionalFields = "hospitalFin, encflag", ErrorMessage = "Got Damn this is complex!")]
[MinLength(6)]
public string HostpitalFinNumber { get; set; }
The value in my view that I was trying to key this validation on is set up like so...
ViewData["ADDENCOREDITTEMP"] = encflag;
if (encflag == "AddEnc"){
isFlagSet = true;
}
I embed it into my page like so...
#Html.Hidden("isFlagSet", isFlagSet, new { id = "isFlagSet"})
I can't get my form to submit... The person who said he just tried this and got it to work, could you post the code?
Model:
public class X
{
[RequiredIf("y", "y", ErrorMessage = "y is not y")]
public string x { get; set; }
public string y { get; set; }
}
View:
#using(Html.BeginForm())
{
#Html.ValidationSummary()
#Html.TextBoxFor(m => m.x)
#Html.TextBoxFor(m => m.y)
<input type="submit"/>
}
I assume your validation fails on the server side? do you have isFlagSet property in your view model?
Here's the situation, I have a list of about 20 properties (called Attributes) that I've defined in my database. This consists of a name, possible values, an optional regex, a boolean that indicates the field is required, etc.
In my ViewModel I get the list of attributes and in my view as List I have a nice EditorTemplate for AttributeViewModel to show them using Steve Sanderson's cool BeginCollectionItem to make sure the post gets bound back to a list of AttributeViewModel (this works just fine).
My AttributeViewModel looks like this:
public class AttributeViewModel
{
public string Description { get; set; }
public IEnumerable<SelectListItem> Values { get; set; }
public string SelectedValue { get; set; }
public byte RenderAs { get; set; }
public int AttributeID { get; set; }
public int ID { get; set; }
public int RegexValidation { get; set; }
public bool IsRequired { get; set; }
}
My View looks like this (edit.cshtml):
#model Company.Services.ViewModels.StaffMemberViewModel
<h2>Edit</h2>
#using (Html.BeginForm())
{
Some fields here, nothing of interest.
#Html.EditorFor(model => model.AttributeValues)
<input type="submit" value="Send" />
}
Here's the interesting bit though, this is my EditorTemplate for AttributeValues:
#using Company.Web.Helpers // This is where "BeginCollectionItem" lives
#model Company.Services.ViewModels.AttributeViewModel
using (Html.BeginCollectionItem("attributes"))
{
<div class="editor-label">
#Model.Description
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.SelectedValue, new SelectList(Model.Values, "Value", "Text"), "-- Select --")
#Html.HiddenFor(model => model.AttributeID)
</div>
}
What I would like to do is use the IsRequired and RegexValidation to make sure the SelectedValue for each attribute is valid. How would I go about doing so? If possible, I'd really like to take advantage of the MVC3 validation framework and unobtrusive validation like I "normally" would.
I obviously can't dynamically add a RequiredAttribute or a RegularExpressionAttribute as these differ for each of my attribute objects in the list.
This is untested. You may have to play with this to get your desired result.
First, create your custom DataAnnotationsModelValidatorProvider class:
public class MyModelMetadataValidatorProvider : DataAnnotationsModelValidatorProvider
{
internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory = Create;
internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories =
new Dictionary<Type, DataAnnotationsModelValidationFactory>()
{
{
typeof(RequiredAttribute),
(metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute)
},
{
typeof(RegularExpressionAttribute),
(metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute)
}
};
internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
{
return new DataAnnotationsModelValidator(metadata, context, attribute);
}
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
List<ModelValidator> vals = base.GetValidators(metadata, context, attributes).ToList();
if (metadata.ModelType.Name == "SelectedValue")
{
// get our parent model
var parentMetaData = ModelMetadataProviders.Current.GetMetadataForProperties(context.Controller.ViewData.Model,
metadata.ContainerType);
// get the associated AttributeId
var attributeId = Convert.ToInt32(parentMetaData.FirstOrDefault(p => p.PropertyName == "AttributeId").Model);
// get AttributeViewModel with specified AttributeId from repository
var attributeViewModel = _db.AttributeViewModels.FirstOrDefault(x => x.AttributeId == attributeId);
DataAnnotationsModelValidationFactory factory;
// check if required
if (attributeViewModel.IsRequired)
{
// must be marked as required
var required = new RequiredAttribute();
required.ErrorMessage = attributeViewModel.Description.Trim() +
" is Required";
if (!AttributeFactories.TryGetValue(required.GetType(), out factory))
factory = DefaultAttributeFactory;
vals.Add(factory(metadata, context, required));
}
// check for regex
if (attributeViewModel.RegexValidation > 0)
{
// get regex from repository
var regexValidation = _db.attributeViewModels.
FirstOrDefault(x => x.RegexValidation == attributeViewModel.RegexValidation);
var regex = new RegularExpressionAttribute(regexValidation.Pattern);
regex.ErrorMessage = attributeViewModel.Description.Trim() +
" is not in a valid format";
if (!AttributeFactories.TryGetValue(regex.GetType(), out factory))
factory = DefaultAttributeFactory;
vals.Add(factory(metadata, context, regex));
}
}
return vals.AsEnumerable();
}
}
Then, add the following to Application_Start in Global.asax.cs:
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MyModelMetadataValidatorProvider());
Consider using FluentValidation.Net (which is available via NuGet from the following Install-Package FluentValidation.MVC3). It makes any sort of relatively complex data validation far simpler and more intuitive than a declarative style. There is support for client-side validation too.
I hope I am understanding your question correctly. You want to add custom validation attributes, annotation and validation logic to your views?
If so, you want to go to the System.ComponentModel.DataAnnotation namespace. Your validation logic will be placed in a class deriving from ValidationAttribute:
using System.ComponentModel.DataAnnotation;
public class MyValidationAttribute : ValidationAttribute
{
string readonly _validationParameter;
public MyValidationAttribute(string validationParameter)
{
_validationParameter = validationParameter;
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
// add validation logic here
if (//not valid)
{
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
return ValidationResult.Success;
}
}
You can apply the attribute to any model property
[Required]
[MyValidationAttribute("parameter", ErrorMessage="Error in {0}")]
public string MyProperty { get; set; }
I hope this helps. See
Professional ASP.NET MVC 3
page 127 for more info.
I'm trying to figure out the valid usages of DisplayAttribute.GroupName property.
MSDN says:
A value that is used to group fields in the UI.
but I wouldn't call it a comprehensive explanation. It makes me think that GroupName can be used to create groupboxes around certain fields. But then the remark:
Do not use this property to get the value of the GroupName property.
Use the GetDescription method instead. A null value or empty string is
valid.
seems to contradict it.
So what is this property for and should I use it (probably with custom template or custom ModelMetadataProvider) in order to render groupboxes around my fields?
In the MVC RTM source code there is no sign of usage.
The "GetDescription" remark might be a copy/paste error in the documentation (each string property seems to have a GetXXX counterpart that returns a localizable value), so it should be most probably "GetGroupName" in this case.
UPDATE:
I would use it exactly for that: group fields together that belong together from the UI point-of-view. As this is just data annotation on the model, it declares only that these fields belong to one logical group "somehow" on the UI, the but concrete presentation details depend on the "UI engine" that displays the model based on the metadata.
I think the most meaningful way to "render" this on the UI is exactly what you said: wrapping the grouped fields into a section or fieldset.
Of course there might be future extensions of MVC or other custom extensions that do some kind of grouping on the UI "automatically" (without writing custom code that examines the metadata and generates the sections) based on this attribute property. But I'm quite sure that such an extension would do something very similar that you would do currently.
I ended up writing this class to make the GroupName more easily accessible:
public class ExtendedDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public const string Key_GroupName = "GroupName";
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
DisplayAttribute displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
if (displayAttribute != null)
modelMetadata.AdditionalValues[ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName] = displayAttribute.GroupName;
return modelMetadata;
}
}
And this extension method:
public static string GetGroupName(this ModelMetadata modelMetadata)
{
if (modelMetadata.AdditionalValues.ContainsKey(ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName))
return (modelMetadata.AdditionalValues[ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName] as string);
return null;
}
Source: http://bradwilson.typepad.com/blog/2010/01/why-you-dont-need-modelmetadataattributes.html
How About This !!! Must Work :
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
namespace System.Web.Mvc
{
public static class DisplayGroup
{
public static MvcHtmlString DisplayGroupName(this HtmlHelper helper, string groupName)
{
return MvcHtmlString.Create(groupName);
}
public static MvcHtmlString DisplayGroupNameFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
var type = typeof(TModel);
PropertyInfo propertyInfo = null;
var member = (MemberExpression)expression.Body;
var property = (PropertyInfo)member.Member;
var name = property.Name;
var metadataTypeInfo = type.GetCustomAttribute<MetadataTypeAttribute>();
if (metadataTypeInfo != null)
{
var metadataType = metadataTypeInfo.MetadataClassType;
propertyInfo = metadataType.GetProperties().Where(x => x.Name == name).FirstOrDefault();
if (propertyInfo == null)
{
propertyInfo = type.GetProperties().Where(x => x.Name == name).FirstOrDefault();
}
}
else
{
propertyInfo = type.GetProperties().Where(x => x.Name == name).FirstOrDefault();
}
string output = "";
var dattr = propertyInfo.GetCustomAttribute<DisplayAttribute>();
if (dattr != null)
{
if (dattr.GroupName == null)
{
output = propertyInfo.Name;
}
else
{
output = dattr.GroupName;
}
}
else
{
output = propertyInfo.Name;
}
return MvcHtmlString.Create(output);
}
}
}
public class MyModel
{
[Display(Name = "Number",GroupName="Invoice")]
string InvNo { get; set; }
}
and then simply write :
#Html.DisplayGroupNameFor(x => x.InvNo)
Note :
NameSpace should be : System.Web.Mvc
Update :
The cool thing is that , if you have a MetaDataType class defined for your dataAnnotation , then also this will work as expected.
I have an extension method that returns an ActionResult (simplified for demonstration purposes):
public static ActionResult GetTestActionResult(this HtmlHelper htmlHelper, int productId)
{
return MVC.Products.Details(productId);
}
I'm using this in an Html.ActionLink:
#Html.ActionLink("Product Details", Html.GetTestActionResult(Model.ProductId), new { #class = "button blue" });
I'm using a custom jQuery plugin for tabs, that uses these hash fragments for navigation. I want to add the tab which I want to open, by tagging the hash fragment onto the end of the URL.
Html.ActionLink does have an overload for the Fragment, namely:
public static MvcHtmlString ActionLink(
this HtmlHelper htmlHelper,
string linkText,
string actionName,
string controllerName,
string protocol,
string hostName,
string fragment,
Object routeValues,
Object htmlAttributes
)
However, that is full of nasty magic strings, which T4MVC is designed to remove. Is there anyway to add the fragment to the route dictionary in my static extension method (GetTestActionResult)?
Something like:
return MVC.Products.Details(productId).AddRouteValue(String.Empty, "#tab-similar-products");
I am aware that there are two similar questions and answers on SO, but they don't quite provide me with what I am looking for. I need to wrap the fragment into the ActionResult BEFORE passing it back to the view:
Including hash values in ASP.NET MVC URL routes
Create a T4MVC ActionLink with url fragment
UPDATE:
Using David Ebbo's fix below, I made the following changes. A bit hacky, but it works:
First I altered my internal function that returns an ActionResult so that it would also add the fragment as a route value (not ideal but works):
return MVC.Products.Details(productId).AddRouteValue("tab", "#tab-similar-products");
Then in the view it copies that fragment value out of the route dictionary, then removes that route value for completeness.
// get my ActionResult with the tab fragment tagged on as a route value
var actionResult = Html.GetTestActionResult(item.Key, Model.ClaimId);
// get the tab fragment value
var tabRoute = actionResult.GetRouteValueDictionary().FirstOrDefault(r => r.Key == "tab").Value ?? "none";
// remove the route value, otherwise it will get tagged to the querystring
actionResult.GetRouteValueDictionary().Remove("tab");
// display
#Html.ActionLink("Product Details", Html.GetTestActionResult(Model.ProductId), new { #class = "button blue" }, fragment: tabRoute.ToString());
I'm sure there is a prettier way to return the fragment with the ActionResult, but for the moment this works. Thanks David.
T4MVC needs new overloads to handle this. In T4MVC.tt, try changing:
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes) {
return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes));
}
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, IDictionary<string, object> htmlAttributes) {
return htmlHelper.RouteLink(linkText, result.GetRouteValueDictionary(), htmlAttributes);
}
to
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes, string protocol = null, string hostName = null, string fragment = null) {
return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes), protocol, hostName, fragment);
}
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, IDictionary<string, object> htmlAttributes, string protocol = null, string hostName = null, string fragment = null) {
return htmlHelper.RouteLink(linkText, null, protocol, hostName, fragment, result.GetRouteValueDictionary(), htmlAttributes);
}
you'll then be able to write something like:
#Html.ActionLink("Product Details", Html.GetTestActionResult(Model.ProductId), new { #class = "button blue" }, fragment: "#tab-similar-products")
Let me know if that works, and I'll try to get it added to the main template.
I used to have something like this:
We suggest you read our #Html.ActionLink("help page", "Help", "Home") before
proceeding.
nice and clean. then I decided we needed to internationalise the app. I couldn't figure out a better way to deal with the above than to store the following string in the resource file:
We suggest you read our [HelpPage] before proceeding.
and then on the view I have to do:
#MvcHtmlString.Create(this.Resource("Help").ToString()
.Replace("[HelpPage]",
#Html.ActionLink("help page", "Help", "Home").ToString()
)
)
What other strategies can you use to internationalize using Razor?
this.Resource() is a page extension that calls .GetLocalResourceObject() and returns an MvcHtmlString
You should create a separate code-behind method that replaces any [placeholder]s with actual links, then call that helper in Razor views.
This will give you a single place to change the code that fills in the links.
I was having the same problem. Instead of using placeholders, I use the same formatting in my resource strings as if I were using String.Format().
An example of using this; my resource strings
LogOnText1 | Please enter your user name and password. {0} if you don't have an account.
LogOnText1Register | Register
and my view (razor):
#MvcHtmlString.Create(String.Format(ViewRes.AccountStrings.LogOnText1,
Html.ActionLink(ViewRes.AccountStrings.LogOnText1Register, "Register")))
I think it's a bit cleaner
so here's what I ended up writing:
public static class PageExtensions
{
public static MvcHtmlString Resource(
this WebViewPage page, string key,
Dictionary<string, MvcHtmlString> tokenMap
) {
HttpContextBase http = page.ViewContext.HttpContext;
string text = (string) http.GetLocalResourceObject(page.VirtualPath, key);
return new TagReplacer(text, tokenMap).ToMvcHtmlString();
}
where the tag replacements gets done like this:
public class TagReplacer
{
Dictionary<string, MvcHtmlString> tokenmap;
public string Value { get; set; }
public TagReplacer(string text, Dictionary<string, MvcHtmlString> tokenMap)
{
tokenmap = tokenMap;
Regex re = new Regex(#"\[.*?\]", RegexOptions.IgnoreCase);
Value = re.Replace(text, new MatchEvaluator(this.Replacer));
}
public string Replacer(Match m)
{
return tokenmap[m.Value.RemoveSet("[]")].ToString();
}
public MvcHtmlString ToMvcHtmlString()
{
return MvcHtmlString.Create(Value);
}
}
so in my code I can now call it like this:
#{
Dictionary<string, MvcHtmlString> tagmap = new Dictionary<string, MvcHtmlString>() {
{ "HelpPage", Html.ActionLink("help page", "Help", "Home") }
};
}
and elsewhere:
#this.Resource("Help", tagmap)
any suggestions for improvement most welcome