Generic Checkbox List View model in MVC - asp.net-mvc

I want to create a generic checkbox list view model and so I got this:
public class ChckboxListViewModel<T>
{
public List<CheckboxViewModel<T>> CheckboxList { get; set; }
public IEnumerable<T> SelectedValues
{
get { return CheckboxList.Where(c => c.IsSelected).Select(c => c.Value); }
}
public ChckboxListViewModel()
{
CheckboxList = new List<CheckboxViewModel<T>>();
}
}
public class CheckboxViewModel<T>
{
public string Label { get; set; }
public T Value { get; set; }
public bool IsSelected { get; set; }
public CheckboxViewModel(string i_Label, T i_Value, bool i_IsSelected)
{
Label = i_Label;
Value = i_Value;
IsSelected = i_IsSelected;
}
}
It is used by a different view model to represent filters of different statuses:
public class FaultListFilters
{
public string SearchKeyword { get; set; }
public ChckboxListViewModel<Fault.eFaultStatus> StatusFilter { get; set; }
public FaultListFilters()
{
SearchKeyword = null;
StatusFilter = new ChckboxListViewModel<Fault.eFaultStatus>();
StatusFilter.CheckboxList.Add(new CheckboxViewModel<Fault.eFaultStatus>(FaultManagementStrings.OpenStatus,Fault.eFaultStatus.Open,true));
StatusFilter.CheckboxList.Add(new CheckboxViewModel<Fault.eFaultStatus>(FaultManagementStrings.InProgressStatus, Fault.eFaultStatus.InProgress, true));
StatusFilter.CheckboxList.Add(new CheckboxViewModel<Fault.eFaultStatus>(FaultManagementStrings.ClosedStatus, Fault.eFaultStatus.Close, false));
}
}
Now I can't find the right way to display the editors or to create an editor template for that kind of a view model because it is Generic.
I don't want o create a separate editor template for ChckboxListViewModel<int> and then another for ChckboxListViewModel<Fault.eFaultStatus> and so on..
Is it even a goose idea to use generics in this case?
Is there another way to represent and display a check-box list in MVC?
I have done the following but the modle is not binding for some reason:
#using (Html.BeginForm("FaultManagement", "Faults", FormMethod.Get, null))
{
for (int i=0 ; i<Model.FaultListFilters.StatusFilter.CheckboxList.Count() ; i++)
{
#Html.HiddenFor(m => m.FaultListFilters.StatusFilter.CheckboxList[i].Value)
#Html.CheckBoxFor(m => m.FaultListFilters.StatusFilter.CheckboxList[i].IsSelected)
#Html.LabelFor(m=> m.FaultListFilters.StatusFilter.CheckboxList[i].IsSelected,Model.FaultListFilters.StatusFilter.CheckboxList[i].Label)
}
<input type="submit" />
}

Is it even a goose idea to use generics in this case?
Don't think it is.
Is there another way to represent and display a check-box list in MVC?
I would write a custom HTML helper:
public static class HtmlExtensions
{
public static IHtmlString CheckboxListFor<TModel>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, IEnumerable<string>>> ex,
IEnumerable<string> possibleValues)
{
var metadata = ModelMetadata.FromLambdaExpression(ex, html.ViewData);
var availableValues = (IEnumerable<string>)metadata.Model;
var name = ExpressionHelper.GetExpressionText(ex);
return html.CheckboxList(name, availableValues, possibleValues);
}
private static IHtmlString CheckboxList(this HtmlHelper html, string name, IEnumerable<string> selectedValues, IEnumerable<string> possibleValues)
{
var result = new StringBuilder();
foreach (string current in possibleValues)
{
var label = new TagBuilder("label");
var sb = new StringBuilder();
var checkbox = new TagBuilder("input");
checkbox.Attributes["type"] = "checkbox";
checkbox.Attributes["name"] = name;
checkbox.Attributes["value"] = current;
var isChecked = selectedValues.Contains(current);
if (isChecked)
{
checkbox.Attributes["checked"] = "checked";
}
sb.Append(checkbox.ToString());
sb.Append(current);
label.InnerHtml = sb.ToString();
result.Append(label);
}
return new HtmlString(result.ToString());
}
}
Then you could have a view model:
public class FaultListFiltersViewModel
{
public IEnumerable<string> SelectedStatusFilters { get; set; }
public IEnumerable<string> AvailableStatusFilters
{
get
{
return new[] { "Label 1", "Label 2", "Label 3" }
}
}
}
and inside the view you could use the helper:
#Html.CheckBoxListFor(x => x.SelectedStatusFilters, Model.AvailableStatusFilters)

Here is another implementation that will better support bootstrap button-group labels (as it requires them to be seperated) and enum type selected values.
public static IHtmlString CheckboxListFor<TModel, TKey>(this HtmlHelper<TModel> helper, Expression<Func<TModel, IEnumerable<TKey>>> ex, Dictionary<TKey, string> i_PossibleOptions, object i_LabelHtmlAttributes)
where TKey : struct, IConvertible
{
var metadata = ModelMetadata.FromLambdaExpression(ex, helper.ViewData);
var selectedValues = (IEnumerable<TKey>)metadata.Model;
var name = ExpressionHelper.GetExpressionText(ex);
return helper.CheckboxList(name, selectedValues, i_PossibleOptions, i_LabelHtmlAttributes);
}
private static IHtmlString CheckboxList<TKey>(this HtmlHelper helper, string name, IEnumerable<TKey> i_SelectedValues, Dictionary<TKey, string> i_PossibleOptions, object i_LabelHtmlAttributes)
where TKey : struct, IConvertible
{
if (!typeof(TKey).IsEnum) throw new ArgumentException("T must be an enumerated type");
var result = new StringBuilder();
foreach (var option in i_PossibleOptions)
{
var label = new TagBuilder("label");
label.MergeAttributes(new RouteValueDictionary(i_LabelHtmlAttributes));
label.Attributes["for"] = string.Format("{0}",option.Key.ToString());
label.InnerHtml = option.Value;
var checkbox = new TagBuilder("input");
checkbox.Attributes["type"] = "checkbox";
checkbox.Attributes["name"] = name;
checkbox.Attributes["id"] = string.Format("{0}", option.Key.ToString());
checkbox.Attributes["value"] = option.Key.ToString();
bool isChecked = ((i_SelectedValues != null) && (i_SelectedValues.Contains(option.Key)));
if ( isChecked )
{
checkbox.Attributes["checked"] = "checked";
}
result.Append(checkbox);
result.Append(label);
}
return new HtmlString(result.ToString());
}
And then the View Model looks like that:
public class FaultListFilters
{
[Display(ResourceType = typeof(FaultManagementStrings), Name = "SearchKeyword")]
public string SearchKeyword { get; set; }
public Dictionary<Fault.eFaultStatus, string> PossibleFaultStatuses
{
get
{
var possibleFaultStatuses = new Dictionary<Fault.eFaultStatus, string>();
possibleFaultStatuses.Add(Fault.eFaultStatus.Open, FaultManagementStrings.OpenStatus);
possibleFaultStatuses.Add(Fault.eFaultStatus.InProgress, FaultManagementStrings.InProgressStatus);
possibleFaultStatuses.Add(Fault.eFaultStatus.Close, FaultManagementStrings.ClosedStatus);
return possibleFaultStatuses;
}
}
public IEnumerable<Fault.eFaultStatus> SelectedFaultStatuses { get; set; }
public FaultListFilters()
{
SearchKeyword = null;
SelectedFaultStatuses = new[] { Fault.eFaultStatus.Open, Fault.eFaultStatus.InProgress };
}
}
and the usage remains the same (except i have added the label html attributes)
<div class="btn-group">
#Html.CheckboxListFor(m => m.FaultListFilters.SelectedFaultStatuses, Model.FaultListFilters.PossibleFaultStatuses, new { Class="btn"})
</div>

Related

asp.net mvc model state errors keys

So I discovered an interesting problem.
I have a model like this:
public class ApplicantModel
{
[Display(Name = "Firstname", ResourceType = typeof(Resources))]
[MaxLength(50, ErrorMessageResourceName = "FirstName", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
[Required(ErrorMessageResourceName = "FirstName", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
public string Firstname { get; set; }
[Display(Name = "Surname", ResourceType = typeof(Resources))]
[MaxLength(50, ErrorMessageResourceName = "Surname", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
[Required(ErrorMessageResourceName = "Surname", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
public string Surname { get; set; }
}
that is all fine, and when I check the Model state and there is an error on a model I get something like this:
errors:
[{
Key = FirstApplicant.Firstname
Value = ["First name is required field"]
},
{
Key = FirstApplicant.Surname
Value = ["Surname name is required field"]
}].
That is also fine.
Edit:
This is the c# ModelState object visualized as JSON object. Real object looks like this:
ModelState
{System.Web.Mvc.ModelStateDictionary}
Count: 2
IsReadOnly: false
IsValid: false
Keys: Count = 2
Values: Count = 2
Results View: Expanding the Results View will enumerate the IEnumerable
However my question is. Is it possible to somehow change the key? I know that the key is created as the name of object and then the name property on that object.
So it makes sense, but is there any way how to change this default behavior? Or do I have to change the names of objects?
Edit2:
What I am trying to achieve here is that I have a c# ViewModel and knockout ViewModel. and when you do server side validations you get this dictionary of keys and values which I serialize and send to client.
And then I call this function on it on client:
var errors = #Html.Raw(Json.Encode(Model.Errors));
function showErrors(serializedErrors) {
var errors = JSON.parse(serializedErrors);
for (var i = 0; i < errors.length; i++) {
var error = errors[i];
var key = error.Key;
var property = eval("masterModel." + key);
property.setError(error.Value.ErrorMessage);
property.isModified(true);
}
}
showErrors(errors);
And this would work fine if the view model property names match on the server and on client. But for example on server side I have a FirstApplicant.FirstName and on a client side it is ApplicantOne.firstname. Thank you all for help and comments. I hope I explained my problem in more detail this time.
in the end I found a solution to this problem. It is a bit complicated but it works.
First I've created an attribute.
public class ClientNameAttribute : Attribute, IMetadataAware
{
public ClientNameAttribute(string name)
{
this.Name = name;
}
public string Name { get; set; }
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.AdditionalValues["ClientName"] = this.Name;
}
}
Notice that this attribute also implements IMetadataAware
Next step was to create Html helper, so I could call this in a view.
public static class HtmlHelperExtensions
{
public static string CustomModelState<T>(this HtmlHelper<T> helper)
{
var errors = helper.ViewData.ModelState.Select(
m => new { Key = GenerateClientName(m.Key, helper), Value = m.Value.Errors.FirstOrDefault() }).Where(e=> e.Value != null);
return Json.Encode(errors);
}
private static string GenerateClientName<T>(string key, HtmlHelper<T> helper)
{
StringBuilder builder = new StringBuilder();
int periodIndex = -1;
do
{
periodIndex = key.IndexOf('.', periodIndex + 1);
string part = key.Substring(0, periodIndex==-1 ? key.Length : periodIndex);
var partMetadata = ModelMetadata.FromStringExpression(part, helper.ViewData);
object clientName;
if (builder.Length > 0)
{
builder.Append('.');
}
if (partMetadata.AdditionalValues.TryGetValue("ClientName", out clientName))
{
builder.Append(clientName);
}
else
{
builder.Append(partMetadata.PropertyName);
}
}
while (periodIndex != -1);
return builder.ToString();
}
}
CustomModelState is a method that I call in a view.
like this:
var errors = #Html.Raw(Html.CustomModelState());
if (errors.length > 0) {
showErrors("masterModel",errors);
}
this will give you nicely formated errors, with your custom names of properties.
And here are tests for it:
public class TestModel
{
[Required]
public string Normal { get; set; }
[ClientName("Other")]
[Required]
public string Changed { get; set; }
[ClientName("Complicated")]
public TestModelTwo TestModelTwo { get; set; }
}
public class TestModelTwo
{
public string PropertyOne { get; set; }
[ClientName("Two")]
public string PropertyTwo{ get; set; }
}
[TestClass]
public class HtmlHelperExtensionsTests
{
[TestMethod]
public void CustomModelStateTests()
{
var model = new TestModel();
var page = new ViewPage();
page.ViewData.Model = model;
page.ViewData.ModelState.AddModelError("Normal", "Error1");
page.ViewData.ModelState.AddModelError("Changed", "Error2");
HtmlHelper<TestModel> helper = new HtmlHelper<TestModel>(new ViewContext(), page);
var custom = helper.CustomModelState();
string expectedResult =
"[{\"Key\":\"Normal\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error1\"}},{\"Key\":\"Other\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error2\"}}]";
Assert.AreEqual(expectedResult, custom);
}
[TestMethod]
public void CustomModelStateTests_ObjectProperty_With_ClientName()
{
var model = new TestModel();
model.TestModelTwo = new TestModelTwo();
var page = new ViewPage();
page.ViewData.Model = model;
page.ViewData.ModelState.AddModelError("TestModelTwo.PropertyOne", "Error1");
page.ViewData.ModelState.AddModelError("TestModelTwo.PropertyTwo", "Error2");
HtmlHelper<TestModel> helper = new HtmlHelper<TestModel>(new ViewContext(), page);
var custom = helper.CustomModelState();
string expectedResult =
"[{\"Key\":\"Complicated.PropertyOne\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error1\"}},{\"Key\":\"Complicated.Two\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error2\"}}]";
Assert.AreEqual(expectedResult, custom);
}
}

MVC 3 CheckboxList and me...Part 3. Try specifying the type arguments explicitly

This IS fun.
Ok, I have the following model(s):
public class My : BusinessCategory
{
[Display(Name = "What types of energy do you use?")]
public List<MyTypes> MyTypeList { get; set; }
public bool? FirstOption { get; set; }
public bool? SecondOption{ get; set; }
public bool? ThirdOption{ get; set; }
public bool? FourthOption { get; set; }
}
Where MyTypes:
public class MyTypes
{
public int MyTypeId { get; set; }
public string MyTypeName { get; set; }
public bool? MyTypeValue { get; set; }
}
My controller is as follows:
public ActionResult My(Guid id)
{
try
{
var model = Model(id);
SetMyTypeList(model.My);
ViewBag.MyTypeMe = new MultiSelectList(model.My.MyTypeList, "MyTypeValue", "MyTypeName");
return View(model.My);
}
catch (Exception ex)
{
ExceptionHelper.WriteLog(ex);
return RedirectToAction("Error");
}
}
private void SetMyTypeList(My model)
{
model.MyTypeList = new List<MyTypes>();
model.MyTypeList.Add(new MyTypes { MyTypeId = 1, MyTypeName = GetName.GetDisplayName(model, m => m.FirstOption), MyTypeValue = model.FirstOption });
model.MyTypeList.Add(new MyTypes { MyTypeId = 2, MyTypeName = GetName.GetDisplayName(model, m => m.SecondOption), MyTypeValue = model.SecondOption});
model.MyTypeList.Add(new MyTypes { MyTypeId = 3, MyTypeName = GetName.GetDisplayName(model, m => m.ThirdOption), MyTypeValue = model.ThirdOption});
model.MyTypeList.Add(new MyTypes { MyTypeId = 4, MyTypeName = GetName.GetDisplayName(model, m => m.FourthOption), MyTypeValue = model.FourthOption });
}
public static string GetDisplayName<TModel, TProperty>(TModel model, Expression<Func<TModel, TProperty>> expression)
{
return ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, new ViewDataDictionary<TModel>(model)).DisplayName;
}
And finally the view is as follows:
#model Valpak.Websites.HealthChecker.Models.My
#{
ViewBag.Title = "My";
}
<h2>
My</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>My Management</legend>
<div style="text-align: left; padding-left: 47%;">
#Html.ListBoxFor(model => model.MyTypeList, ViewBag.MyTypeMe as MultiSelectList)
#Html.CheckBoxListFor(model => model.MyTypeList, ViewBag.EnergyTypeMe as MultiSelectList, Model.ReviewId)
</div>
<p>
<input type="submit" value="Continue" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Cancel and return to categories", "BusinessSummary", new { id = Model.ReviewId })
</div>
CheckboxListFor, if it was working, would use the following extension:
public static class HtmlHelper
{
//Extension
public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty[]>> expression, MultiSelectList multiSelectList, object htmlAttributes = null)
{
//Derive property name for checkbox name
MemberExpression body = expression.Body as MemberExpression;
string propertyName = body.Member.Name;
//Get currently select values from the ViewData model
TProperty[] list = expression.Compile().Invoke(htmlHelper.ViewData.Model);
//Convert selected value list to a List<string> for easy manipulation
List<string> selectedValues = new List<string>();
if (list != null)
{
selectedValues = new List<TProperty>(list).ConvertAll<string>(delegate(TProperty i) { return i.ToString(); });
}
//Create div
TagBuilder divTag = new TagBuilder("div");
divTag.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);
//Add checkboxes
foreach (SelectListItem item in multiSelectList)
{
divTag.InnerHtml += String.Format("<div><input type=\"checkbox\" name=\"{0}\" id=\"{0}_{1}\" " +
"value=\"{1}\" {2} /><label for=\"{0}_{1}\">{3}</label></div>",
propertyName,
item.Value,
selectedValues.Contains(item.Value) ? "checked=\"checked\"" : "",
item.Text);
}
return MvcHtmlString.Create(divTag.ToString());
}
}
Can someone you explain in very simplistic terms (I’m a bit dense) why I can use the ListBoxFor example but this dies and gives me the following error when I use the checkbox?
CS0411: The type arguments for method 'Extensions.HtmlHelper.CheckBoxListFor<TModel,TProperty>(System.Web.Mvc.HtmlHelper<TModel>, System.Linq.Expressions.Expression<System.Func<TModel,TProperty[]>>, System.Web.Mvc.MultiSelectList, System.Guid, object)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
Can anyone offer any sort of work around as I’d quite like to use my :’(
As always, apologies for my ignorance.
In the signature of the extension method you have the following second argument:
Expression<Func<TModel, TProperty[]>> expression,
This basically means that the expression must return an array of TProperty => TProperty[]
whereas in your view model you have a List<T>:
public List<MyTypes> EnergyTypeList { get; set; }
and inside your view you are using:
model => model.EnergyTypeList
Your code doesn't work because List<EnergyTypeList> is not the same thing as EnergyTypeList[].
So you have different possibilities. Either change the type in your view model to match the one in your helper or use change your helper to use a List or even better an IEnumerable<TProperty>. This way the extension method will work even with arrays.

Semi-Complex View Model Property Validation in ASP.NET MVC 3

I am struggling to complete a server-client validation solution for a semi-complex scenario. I have a core type called DateRange:
public class DateRange {
public DateRange (DateTime? start, DateTime? end) { ... }
public DateTime? Start { get; private set; }
public DateTime? End { get; private set; }
}
I have a view model like:
public class MyViewModel {
public DateRange Period { get; set; }
}
I have a %mvcproject%\Views\Shared\EditorTemplates\DateRange.cshtml like:
#model MyCore.DateRange
#Html.Editor("Start", "Date")
#Html.Editor("End", "Date")
I also have a DateRangeModelBinder to bind the two form inputs into the DateRange property. The problem I'm having is with a DateRangeRequiredAttribute:
public class DateRangeRequired : ValidationAttribute, IClientValidatable,
IMetadataAware
{
private const string DefaultErrorMessage =
"{0} is required.";
public DateRangeRequired(bool endIsRequired = true)
: base(() => DefaultErrorMessage)
{
EndIsRequired = endIsRequired;
}
public bool EndIsRequired { get; set; }
public override bool IsValid(object value)
{
if (value == null)
{
return false;
}
if (!value.GetType().IsAssignableFrom(typeof(DateRange)))
{
throw new ArgumentException("Value is not a DateRange.");
}
var dateRange = value as DateRange;
return (dateRange.Start.HasValue && !EndIsRequired) ||
(dateRange.Start.HasValue && dateRange.End.HasValue && EndIsRequired);
}
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "daterangerequired"
};
rule.ValidationParameters.Add("endisrequired", EndIsRequired.ToString().ToLower());
yield return rule;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.DataTypeName = "DateRange";
}
}
I can't get it to hook up to the two inputs. It's almost like there needs to be a ValidatorTemplate that pairs with the EditorTemplate because of the split inputs. Any ideas? Let me know if additional clarification is needed.
You haven't shown exactly how your custom DateRangeRequiredAttribute implementation looks like, so let me suggest an example:
public class DateRangeRequiredAttribute : ValidationAttribute, IClientValidatable
{
private readonly string _otherProperty;
public DateRangeRequiredAttribute(string otherProperty)
{
_otherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_otherProperty);
if (property == null)
{
return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "Unknown property {0}", _otherProperty));
}
var otherValue = property.GetValue(validationContext.ObjectInstance, null);
if (!(value is DateTime) || !(otherValue is DateTime))
{
return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "The two properties to compare must be of type DateTime"));
}
if ((DateTime)value >= (DateTime)otherValue)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "daterange"
};
rule.ValidationParameters.Add("other", "*." + _otherProperty);
yield return rule;
}
}
then you could decorate your view model with it:
public class DateRange
{
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
[DateRangeRequired("End", ErrorMessage = "Please select a start date before the end date")]
public DateTime? Start { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
[Required]
public DateTime? End { get; set; }
}
and finally in the view register the adapter:
jQuery.validator.unobtrusive.adapters.add(
'daterange', ['other'], function (options) {
var getModelPrefix = function (fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
};
var appendModelPrefix = function (value, prefix) {
if (value.indexOf('*.') === 0) {
value = value.replace('*.', prefix);
}
return value;
};
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(':input[name="' + fullOtherName + '"]')[0];
options.rules['daterange'] = element;
if (options.message) {
options.messages['daterange'] = options.message;
}
}
);
jQuery.validator.addMethod('daterange', function (value, element, params) {
// TODO: some more advanced date checking could be applied here
// currently it uses the current browser culture setting to perform
// the parsing. If you needed to use the server side culture, this code
// could be adapted respectively
var date = new Date(value);
var otherDate = new Date($(params).val());
return date < otherDate;
}, '');
After reading this pornography, you might consider using FluentValidation.NET which renders this extremely simple validation scenario a couple of lines to implement (which is how such simple validation scenarios should be done). I would strongly recommend you this library. I am using it in all my projects because I am sick of DataAnnotations for validation. They are so pretty limited.

MVC3 Tabs Helper

I need an html helper that would take care of "tabs" functionality on a page. clicking on the tabs will re-load the page and reload the partial view (if specified). I wrote it like so but not sure it this is the best solution.??
public static class TabExtensions
{
public static MvcHtmlString Tabs(this HtmlHelper htmlHelper, List<TabItem> tabItems, object htmlAttributes = null)
{
if (tabItems == null)
{
throw new ArgumentException("at least one tab item required");
}
string viewName = string.Empty;
object model = null;
var sb = new StringBuilder();
sb.Append("<a name=\"tabs\"></a>");
var tagUl = new TagBuilder("ul");
tagUl.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
// Current url data
var baseUri = new UriBuilder(htmlHelper.ViewContext.HttpContext.Request.Url);
var selTab = htmlHelper.ViewContext.RequestContext.HttpContext.Request.QueryString["tab"];
foreach (var tab in tabItems)
{
// No tab user selected
if (string.IsNullOrEmpty(selTab))
{
selTab = tab.TabLinkText;
}
var tagLi = new TagBuilder("li");
string tagInnerHtml;
if (selTab.Equals(tab.TabLinkText, StringComparison.OrdinalIgnoreCase))
{
tagLi.MergeAttribute("class", "current");
tagInnerHtml = string.Format("<strong>{0}</strong>", tab.Text);
viewName = tab.PartialViewName;
model = tab.PartialViewModel;
}
else
{
tagInnerHtml = tab.Text;
}
var queryToAppend = string.Concat("tab=", tab.TabLinkText);
var querystring = new StringBuilder();
if (baseUri.Query.Length > 1)
{
if (baseUri.Query.Contains("tab"))
{
querystring.Append(baseUri.Query.Replace(string.Concat("tab=", selTab), queryToAppend));
}
else
{
querystring.Append(baseUri.Query + "&" + queryToAppend);
}
}
else
{
querystring.Append("?" + queryToAppend);
}
// Assign anchor link
querystring.Append("#tabs");
tagLi.InnerHtml = string.Format("{1}", querystring, tagInnerHtml);
tagUl.InnerHtml += tagLi.ToString();
}
sb.Append(tagUl.ToString());
// Render partial
if (!string.IsNullOrEmpty(viewName))
{
htmlHelper.ViewData.Model = model;
using (StringWriter sw = new StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(htmlHelper.ViewContext.Controller.ControllerContext, viewName);
ViewContext viewContext = new ViewContext(htmlHelper.ViewContext.Controller.ControllerContext, viewResult.View, htmlHelper.ViewData, htmlHelper.ViewContext.TempData, sw);
viewResult.View.Render(viewContext, sw);
sb.Append(sw.GetStringBuilder().ToString());
}
}
return MvcHtmlString.Create(sb.ToString());
}
}
#region Tab Item Model
public class TabItem
{
public string Text { get; set; }
public string TabLinkText { get; set; }
public string PartialViewName { get; set; }
public object PartialViewModel { get; set; }
public TabItem(string text, string tabLinkText)
: this()
{
this.Text = text;
this.TabLinkText = tabLinkText;
}
public TabItem(string text, string tabLinkText, string partialViewName, object partialViewModel = null)
: this()
{
this.Text = text;
this.TabLinkText = tabLinkText;
this.PartialViewName = partialViewName;
this.PartialViewModel = partialViewModel;
}
public TabItem()
{
this.Text = string.Empty;
this.PartialViewName = string.Empty;
this.TabLinkText = string.Empty;
this.PartialViewModel = null;
}
}
#endregion
You use it like so:
<%
var tabList = new List<TabItem>
{
new TabItem(LocalResources.fld_AboutFirm_lbl, "about"),
new TabItem(LocalResources.fld_FirmOffers_lbl, "offer"),
new TabItem(LocalResources.fld_Profile_lbl, "profile", "~/Views/Partial/FirmProfileTab.cshtml", Model),
new TabItem(LocalResources.fld_Contact_lbl, "contact", "~/Views/Partial/FirmContactTab.cshtml", Model)
};
%>
<%: Html.Tabs(tabList, new { #class = "firmTabs clearfix" })%>
this will generate html:
<ul class="firmTabs clearfix"><li>O firmie</li><li>Firma oferuje</li><li>Profil</li><li class="current"><strong>Kontakt</strong></li></ul>
I think the better solution is to make master page for tab menu and view pages for tab content. Your approach looks very complex for me. Why do you need html helper for this? If you encapsulate your html into helper method - you loose your view. So in terms of MVC your way is not good I think.

Unique constraint with data annotation

I'm using the System.ComponentModel.DataAnnotations namespace to validate my domain classes. How can I create a custom attribute to validate the uniqueness of a property regardless of the database (through some interface, for example)?
This is the solution I came up with for this situation, it simply checks the table for a record with a different id that has the same value for the property being validated. It assumes that you will be using LinqToSQL, and that any table on which this kind of validation is required has a single ID column.
I'd also put a unique constraint on the underlying table in the database. This attribute allows me to put a nice error message on the form and associate it with the appropriate property.
public class UniqueAttribute : ValidationAttribute
{
public Func<DataContext> GetDataContext { get; private set; }
public string IDProperty { get; private set; }
public string Message { get; private set; }
public UniqueAttribute(Type dataContextType, string idProperty, string message)
{
IDProperty = idProperty;
Message = message;
GetDataContext = () => (DataContext)Activator.CreateInstance(dataContextType);
}
public UniqueAttribute(Type dataContextType, string idProperty, string message, string connectionString)
{
IDProperty = idProperty;
Message = message;
GetDataContext = () => (DataContext)Activator.CreateInstance(dataContextType, new object[] { connectionString });
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var idProperty = validationContext.ObjectType.GetProperty(IDProperty);
var idType = idProperty.PropertyType;
var id = idProperty.GetValue(validationContext.ObjectInstance, null);
// Unsightly hack due to validationContext.MemberName being null :(
var memberName = validationContext.ObjectType.GetProperties()
.Where(p => p.GetCustomAttributes(false).OfType<DisplayAttribute>().Any(a => a.Name == validationContext.DisplayName))
.Select(p => p.Name)
.FirstOrDefault();
if (string.IsNullOrEmpty(memberName))
{
memberName = validationContext.DisplayName;
}
// End of hack
var validateeProperty = validationContext.ObjectType.GetProperty(memberName);
var validateeType = validateeProperty.PropertyType;
var validatee = validateeProperty.GetValue(validationContext.ObjectInstance, null);
var idParameter = Expression.Constant(id, idType);
var validateeParameter = Expression.Constant(validatee, validateeType);
var objectParameter = Expression.Parameter(validationContext.ObjectType, "o");
var objectIDProperty = Expression.Property(objectParameter, idProperty);
var objectValidateeProperty = Expression.Property(objectParameter, validateeProperty);
var idCheck = Expression.NotEqual(objectIDProperty, idParameter);
var validateeCheck = Expression.Equal(objectValidateeProperty, validateeParameter);
var compositeCheck = Expression.And(idCheck, validateeCheck);
var lambda = Expression.Lambda(compositeCheck, objectParameter);
var countMethod = typeof(Queryable).GetMethods().Single(m => m.Name == "Count" && m.GetParameters().Length == 2);
var genericCountMethod = countMethod.MakeGenericMethod(validationContext.ObjectType);
using (var context = GetDataContext())
{
var table = context.GetTable(validationContext.ObjectType) as IQueryable<Models.Group>;
var count = (int)genericCountMethod.Invoke(null, new object[] { table, lambda });
if (count > 0)
{
return new ValidationResult(Message);
}
}
return null;
}
}
Example usage:
[MetadataType(typeof(UserMetadata))]
public partial class Group : IDatabaseRecord
{
public class UserMetadata
{
[Required(ErrorMessage = "Name is required")]
[StringLength(255, ErrorMessage = "Name must be under 255 characters")]
[Unique(typeof(MyDataContext), "GroupID", "Name must be unique")]
public string Name { get; set; }
}
}
just do something like this on your model
[StringLength(100)]
[Index("IX_EntidadCodigoHabilitacion", IsUnique = true)]
public string CodigoHabilitacion { get; set; }
If I am understanding you properly, you should be able to create a custom ValidationAttribute and get a context to your repository through a custom factory.
Validator:
using System.ComponentModel.DataAnnotations;
public class DBUniqueAttribute : ValidationAttribute
{
private IRepository Repository{ get; set;}
public DBUniqueAttribute()
{
this.Repository = MyRepositoryFactory.Create();
}
public override bool IsValid(object value)
{
string stringValue = Convert.ToString(value, CultureInfo.CurrentCulture);
return Repository.IsUnique(stringValue);
}
}
You would have an IRepository interface with an IsUnique() method. The MyRepositoryFactory would have a static method called Create() which would create the concrete Repository necessary for your database. If the database type changes, you only need to update the Factory to return a new Repository for your new database.
I love #daveb's solution. Unfortunately, three years later it required some pretty heavy modification for me. Here's his solution updated for EF6. Hopefully will save someone an hour or so of fiddling.
public class UniqueAttribute : ValidationAttribute
{
public UniqueAttribute(string idProperty, string message)
{
IdProperty = idProperty;
Message = message;
}
[Inject]
public DataContext DataContext { get; set; }
private string IdProperty { get; set; }
private string Message { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var objectType = validationContext.ObjectType;
if (objectType.Namespace == "System.Data.Entity.DynamicProxies")
{
objectType = objectType.BaseType;
}
var idProperty = objectType.GetProperty(IdProperty);
var idType = idProperty.PropertyType;
var id = idProperty.GetValue(validationContext.ObjectInstance, null);
var memberName = validationContext.MemberName;
var validateeProperty = objectType.GetProperty(memberName);
var validateeType = validateeProperty.PropertyType;
var validatee = validateeProperty.GetValue(validationContext.ObjectInstance, null);
var idParameter = Expression.Constant(id, idType);
var validateeParameter = Expression.Constant(validatee, validateeType);
var objectParameter = Expression.Parameter(objectType, "o");
var objectIdProperty = Expression.Property(objectParameter, idProperty);
var objectValidateeProperty = Expression.Property(objectParameter, validateeProperty);
var idCheck = Expression.NotEqual(objectIdProperty, idParameter);
var validateeCheck = Expression.Equal(objectValidateeProperty, validateeParameter);
var compositeCheck = Expression.And(idCheck, validateeCheck);
var lambda = Expression.Lambda(compositeCheck, objectParameter);
var countMethod = typeof(Queryable).GetMethods().Single(m => m.Name == "Count" && m.GetParameters().Length == 2);
var genericCountMethod = countMethod.MakeGenericMethod(objectType);
var table = DataContext.Set(objectType);
var count = (int)genericCountMethod.Invoke(null, new object[] { table, lambda });
if (count > 0)
{
return new ValidationResult(Message);
}
return null;
}
}

Resources