I need to validate a byte[] in my model as Required but whenever I use Data Annotation [Required] on it, it won't do anything. Even if i choose a file it outputs error message.
Details:
Model:
Public class MyClass
{
[Key]
public int ID {get; set;}
[Required]
public string Name {get; set;}
public byte[] Image {get; set;}
[Required]
public byte[] Template {get; set;}
}
View:
<div class="editor-label">
<%:Html.LabelFor(model => model.Image) %>
</div>
<div class="editor-field">
<input type="file" id="file1" name="files" />
</div>
<div class="editor-label">
<%:Html.Label("Template") %>
</div>
<div class="editor-field">
<input type="file" id="file2" name="files"/>
</div>
<p>
<input type="submit" value="Create" />
</p>
I've looked around the posts and notice people use custom validation but they have used HttpPostedFileBase as types of files instead of byte[] like me, for some reason when I try to use the same it errors with a missing ID for it... Even though the model has it's own ID declared.
EDIT:
Context - OnModelCreating additions for Report
modelBuilder.Entity<Report>().Property(p => p.Image).HasColumnType("image");
modelBuilder.Entity<Report>().Property(p => p.Template).HasColumnType("image");
note that i had to put image as ColumnType because of Byte array truncation to a length of 4000. error.
Controller:
public ActionResult Create(Report report, IEnumerable<HttpPostedFileBase> files)
{
if (ModelState.IsValid)
{
db.Configuration.ValidateOnSaveEnabled = false;
if (files.ElementAt(0) != null && files.ElementAt(0).ContentLength > 0)
{
using (MemoryStream ms = new MemoryStream())
{
files.ElementAt(0).InputStream.CopyTo(ms);
report.Image = ms.GetBuffer();
}
}
if (files.ElementAt(1) != null && files.ElementAt(1).ContentLength > 0)
{
using (MemoryStream ms1 = new MemoryStream())
{
files.ElementAt(1).InputStream.CopyTo(ms1);
report.Template = ms1.GetBuffer();
}
}
db.Reports.Add(report);
db.SaveChanges();
//Temporary save method
var tempID = 10000000 + report.ReportID;
var fileName = tempID.ToString(); //current by-pass for name
var path = Path.Combine(Server.MapPath("~/Content/Report/"), fileName);
files.ElementAt(1).SaveAs(path);
db.Configuration.ValidateOnSaveEnabled = true;
return RedirectToAction("Index");
}
Hopefully you may notice what I'm missing.
The RequiredAttribute checks for null and an empty string.
public override bool IsValid(object value)
{
if (value == null)
return false;
string str = value as string;
if (str != null && !this.AllowEmptyStrings)
return str.Trim().Length != 0;
else
return true;
}
This works fine if your byte array is null, but you probably want to check for an empty array as well (without seeing how you are assigning the value of your Template property, I can only guess that this is the case). You can define your own required attribute that does this check for you.
public class RequiredCollectionAttribute : RequiredAttribute
{
public override bool IsValid(object value)
{
bool isValid = base.IsValid(value);
if(isValid)
{
ICollection collection = value as ICollection;
if(collection != null)
{
isValid = collection.Count != 0;
}
}
return isValid;
}
}
Now just replace the Required attribute on your Template property with our new RequiredCollection attribute.
[RequiredCollection]
public byte[] Template {get; set;}
I've looked around the posts and notice people use custom validation
but they have used HttpPostedFileBase as types of files instead of
byte[] like me..
Do you want to bind the posted file to a byterray field in the model? If yes you have to go for a custom model binder.
ASP.NET MVC already has a built-in model binder for byte array so you can easily extend that as suggested in this post.
public class CustomFileModelBinder : ByteArrayModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var file = controllerContext.HttpContext.Request.Files[bindingContext.ModelName];
if (file != null)
{
if (file.ContentLength != 0 && !String.IsNullOrEmpty(file.FileName))
{
var fileBytes = new byte[file.ContentLength];
file.InputStream.Read(fileBytes, 0, fileBytes.Length);
return fileBytes;
}
return null;
}
return base.BindModel(controllerContext, bindingContext);
}
}
protected void Application_Start()
{
...
ModelBinders.Binders.Remove(typeof(byte[]));
ModelBinders.Binders.Add(typeof(byte[]), new CustomFileModelBinder());
}
Now the uploaded file will directly come and sit as bytearray in the properties.
I've altered my Create method, and this is what I've come up with. Seems to work fine, but...
if (files.ElementAt(1) != null && files.ElementAt(1).ContentLength > 0)
{
using (MemoryStream ms1 = new MemoryStream())
{
files.ElementAt(1).InputStream.CopyTo(ms1);
report.Template = ms1.GetBuffer();
}
}
else // this part of code did the trick, although not sure how good it is in practice
{
return View(report);
}
Related
I have some tabs in bootstrap which has to be set as active depending on the action being hit. I have sub-tabs as well which has to be set as active depending on the action being hit as well.
Here is an image of how it looks like:
So when a sub-tab is being active the parent tab has to be active too.
So I thought to create a new Attribute where I save a pageId for each action and depending on the pageId on the view I can set it to active or not:
Here is the attribute:
public class YcoPageId : Attribute
{
public YcoPageId(int pageId)
{
PageId = pageId;
}
public int PageId { get; }
}
Here is the action:
[YcoPageId(1)]
public ActionResult Admin()
{
return View();
}
For the view I want to create an extension method to see if the tab and sub-tabs shall be active or not!
Here is my code:
public static bool IsActive(this HtmlHelper htmlHelper, params int[] ids)
{
var viewContext = htmlHelper.ViewContext;
var action = viewContext....
//How to get the YcoPageId attribute from here and see the Id
//Here i will test if ids contain id but dont know how to get it...
}
If you think adding an id for each page is bad idea I think for my case I will use this id for other purposes as well because it will be like identifier for a specific action...
So my question is how can I get the attribute YcoPageId for current action in my extension method ?
The view will look like this:
<li class="#(Html.IsActive(1, 4, 5... etc)? "active" : "")">
<a href="#url">
<div class="row text-center">
<div class="col-md-12">
<i class="fa #fontAwesomeIcon fa-4x" aria-hidden="true"></i>
<br/>#menuName
</div>
</div>
</a>
</li>
If there is any better idea how to solve this issue please go ahead!
Here is my solution to this problem:
First created a actionfilter attribute like below:
public class YcoPageIdAttribute : ActionFilterAttribute
{
public YcoPageIdAttribute(int pageId)
{
PageId = pageId;
}
public int PageId { get; }
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Result is ViewResult)
{
filterContext.Controller.TempData[DomainKeys.ViewPageId] = PageId;
}
else
{
throw new Exception("Only ViewResult has unique id");
}
base.OnActionExecuted(filterContext);
}
}
Then my action would look like this one:
[YcoPageId(1)]
public ActionResult Admin()
{
return View();
}
I created an extension method like below:
public static bool IsActive(this HtmlHelper htmlHelper, params int[] ids)
{
var viewContext = htmlHelper.ViewContext;
return viewContext.TempData.ContainsKey(DomainKeys.ViewPageId) &&
int.Parse(viewContext.TempData.Peek(DomainKeys.ViewPageId).ToString()).In(ids);
}
Since I know the id of an action now I have only to put the code as below in the view:
<li class="#(Html.IsActive(1)? "active" : "")">
<a href="#url">
<div class="row text-center">
<div class="col-md-12">
<i class="fa #fontAwesomeIcon" aria-hidden="true"></i>
<br /><small>#menuName</small>
</div>
</div>
</a>
</li>
I made another method on startup to check if I have actions with duplicated values like below:
public static void CheckForDuplicateActionId()
{
Assembly asm = Assembly.GetExecutingAssembly();
var controllerActionlist = asm.GetTypes()
.Where(type => typeof(Controller).IsAssignableFrom(type))
.SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |
BindingFlags.Public))
.Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute),
true).Any())
.Where(m => m.GetCustomAttribute<YcoPageIdAttribute>() != null)
.Select(
x =>
new
{
Controller = x.DeclaringType.Name,
Area = x.DeclaringType.FullName,
Action = x.Name,
ReturnType = x.ReturnType.Name,
Id = x.GetCustomAttribute<YcoPageIdAttribute>().PageId
})
.ToList();
var actionIds = controllerActionlist.Select(x => x.Id).ToList();
var actionIdsGrouped = actionIds.GroupBy(x => x).Where(x => x.Count() > 1).ToList();
if (!actionIdsGrouped.IsNullOrEmpty())
{
StringBuilder error = new StringBuilder("");
actionIdsGrouped.ForEach(actionId =>
{
var actions = controllerActionlist.Where(x => x.Id == actionId.Key);
actions.ForEach(a =>
{
error.Append(
$" | Id : {a.Id}, Action Name: {a.Action}, Controller Name : {a.Controller}, Location : {a.Area}. ");
});
});
var maxId = controllerActionlist.Max(x => x.Id);
error.Append(
"PLease consider changing the the duplicated id - Here are some options to choose from : Id {");
for (int i = 1, j = 1; i < maxId + 5; i++)
{
if (actionIds.Contains(i)) continue;
if (j < 5)
{
error.Append(i + ",");
j++;
}
else
{
error.Append(i + "}");
break;
}
}
throw new Exception(
$"There are more than one action duplicated with the same Id, The action data are as below : {error}");
}
}
Probably I will add all these data in database so I can identify an action from one id from database as well :)
Now it is working good.
If I understand correctly you are trying to create an id for each page/view and use it in the page/view to dynamically set css classes for setting menu tabs as active. If that is the case... Rather than trying to set the Ids in the Controller, how about creating a Shared View with only the following code - something like this....
In the View write the following Razor/C# code.
#{
var iPageId = 0
var sViewPath = ((System.Web.Mvc.BuildManagerCompiledView)ViewContext.View).ViewPath;
//for example
if (sViewPath.ToLower.IndexOf("admin") >= 0)
{
iPageId = 1;
}
else if (sViewPath.ToLower.IndexOf("dashboard") >= 0)
{
iPageId = 2;
}
else if (sViewPath.ToLower.IndexOf("vessels") >= 0)
{
iPageId = 3;
}
else if (sViewPath.ToLower.IndexOf("reports") >= 0)
{
iPageId = 4;
}
}
Render the Shared View in the primary view with the following snippet.
#Html.Partial("~/Views/Menu/_SharedViewName.cshtml")
Then you should be able to access the iPageId variable anywhere in the primary page/view(s) and set your CSS classes accordingly.
While developing an ASP.Net MVC 4 application, I have followed the answer on pass enum to html.radiobuttonfor MVC3 and the answer from which it was derived, but my problem remains.
I am creating an edit page, and want to display Status as two radio buttons "Active" and "InActive", these values are read from the database as Enums 1 = Active , 2 = InActive.
The problem is, when the page is displayed it show's the correct radio button selected corresponding to the database value, but will not allow the user to change/select the other radio button ??
Any idea's, it's driving me nuts
(also changing the property to a bool for a chkbox will cause more problems than it will solve at this point. )
Controller.....
[HttpPost]
public ActionResult Edit(NewsArticle newsArticle, int id, HttpPostedFileBase Article)
{
try
{
if (ModelState.IsValid)
{
NewsArticle savedArticle= _newsArticle.Get(id);
savedArticle.Body = newsArticle.Body;
savedArticle.Title = newsArticle.Title;
savedArticle.Status = newsArticle.Status;
if(Article == null)
{
newsArticle.ArticleImage = savedArticle.ArticleImage;
}
else
{
using (var binaryReader = new BinaryReader(Request.Files[0].InputStream))
{
newsArticle.ArticleImage = binaryReader.ReadBytes(Request.Files[0].ContentLength);
}
savedArticle.ArticleImage = newsArticle.ArticleImage;
string imgeName = Path.GetFileName(Article.FileName);
savedArticle.ImageName = imgeName;
}
_uow.SaveChanges();
return RedirectToAction("Index");
}
View..........
<div class="control-group">
<div class="editor-field">
<label class="control-label">Select Status :</label>
<div class="controls">
#Html.RadioButtonForEnum(n => n.Status)
</div>
</div>
</div>
Helper/extension.....
public static MvcHtmlString RadioButtonForEnum<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var names = Enum.GetNames(metaData.ModelType);
var sb = new StringBuilder();
foreach (var name in names)
{
var description = name;
var memInfo = metaData.ModelType.GetMember(name);
if (memInfo != null)
{
var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
if (attributes != null && attributes.Length > 0)
description = ((DisplayAttribute)attributes[0]).Name;
}
var id = string.Format(
"{0}_{1}_{2}",
htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix,
metaData.PropertyName,
name
);
var radio = htmlHelper.RadioButtonFor(expression, name, new { id = id }).ToHtmlString();
sb.AppendFormat(
"<label for=\"{0}\">{1}</label> {2}",
id,
HttpUtility.HtmlEncode(name),
radio
);
}
return MvcHtmlString.Create(sb.ToString());
}
The helper is displaying labels as it should having being called from the view.
Enum....
[Flags]
public enum NewsArticleStatus
{
[Display(Name = "Active")]
Active = 1,
[Display(Name = "InActive")]
Inactive = 2
}
Got it....
Added my original model(Article) to a viewModel and Added a bool property.... Active..
public bool Active
{
get
{
if (this.Article != null)
{
return (this.Article.Status == NewsArticleStatus.Active);
}
return false;
}
set
{
if (this.Article != null)
{
this.Article.Status = (value ? NewsArticleStatus.Active : NewsArticleStatus.Inactive);
}
}
}
Create the chkbox....
#Html.CheckBoxFor(n => n.Active)
So in the Controller simply assign the value to the new model
newNews.Status = model.Article.Status;
My model class:
public class StatusList
{
public int StatusID {get;set;}
[UIHint("ByteCheckbox")]
public byte Active {get;set;}
}
In /Views/Shared/EditorTemplates I created a file called ByteCheckbox.cshtml
The editortemplate ByteCheckbox contains (My 3rd attempt):
#model byte
#if (Model == 1)
{
#Html.CheckBox("", true)
}
else
{
#Html.CheckBox("", false)
}
Doing this nicely renders a checkbox. When I change the checkbox status and try to save the changes the model validation complains that the value is 'false' (or 'true') instead of the expected 0 or 1.
How to modify the editortemplate to allow for the value to be translated?
Have you tried this?
<div class="editor-label">
#Html.LabelFor(model => model.Active)
</div>
<div class="editor-field">
#Html.CheckBoxFor(model => model.Active != 0)
#Html.ValidationMessageFor(model => model.Active)
</div>
You can do this in your model:
public class StatusList
{
public int StatusID {get;set;}
public byte Active {get;set;}
[NotMapped]
public bool ActiveBool
{
get { return Active > 0; }
set { Active = value ? 1 : 0; }
}
}
Don't use Html.CheckBox; instead use Html.EditorFor. You'll need to define a file called ByteCheckbox.cshtml in Views/Shared/EditorTemplates for this to work as well.
You can also use a custom model binder.
Here's a sample for a decimal, but you can do it for the byte type.
public class DecimalModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
dynamic valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null) {
return base.BindModel(controllerContext, bindingContext);
}
return ((string)valueProviderResult.AttemptedValue).TryCDec();
}
}
try this one
#model bool
<div class="editor-for">
#Html.CheckBox("", Model, new{#class="tickbox-single-line"})
<div>
Try this:
#Html.CheckBox("Model", "Model")
In my viewmodel, I have a list of items I fetch from the database and then send to the view. I would like to know if it's possible to avoid having to refill the options property whenever I hit a Post action and need to return the model (for validation errors and what not)?
In webforms, this wouldn't be necessary.
Edit: I was not clear. My problem is with the SelectList options I use for my DropDownLists. Everything gets posted, but if I have to return to the view (model is invalid), I have to reload the options from the database! I want to know if this can be avoided.
My viewmodel:
public class TestModel
{
public TestModel()
{
Departments = new List<SelectListItem>();
}
public string Name { get; set; }
public int Department { get; set; }
public IEnumerable<SelectListItem> Departments { get; set; }
}
My view:
#model MvcApplication1.Models.TestModel
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.Name)
#Html.DropDownListFor(m => m.Department, Model.Departments)
<input type=submit value=Submit />
}
My controller (do notice the comment on HttpPost):
public ActionResult Index()
{
TestModel model = new TestModel
{
Name = "Rafael",
Department = 1,
Departments = new List<SelectListItem>
{
new SelectListItem { Text = "Sales", Value = "1" },
new SelectListItem { Text = "Marketing", Value = "2", Selected = true },
new SelectListItem { Text = "Development", Value = "3" }
}
};
// Departments gets filled from a database.
return View(model);
}
[HttpPost]
public ActionResult Index(TestModel model)
{
if (!ModelState.IsValid)
{
//Do I have to fill model.Departments again!?!?!?
return View(model);
}
else { ... }
}
Thanks in advance.
Edit: FYI, my solution was to use the Session variable.
Just need to strongly type your view, and change your controller method to have a parameter of that class type.
That is, the view
#model MyNamesspace.Models.MyModel
...
#using (Html.BeginForm())
{
....
}
And you controller method which is posted to.
[HttpPost]
public ActionResult MyAction(MyModel model)
{
...
}
EDIT: Also make sure you have form fields for each property of the model which you need posted to the controller. My example is using Razor too BTW.
I encountered a similar problem when trying to create an Order wizard in MVC (one where each page of the wizard is implemented as a partial view loaded by AJAX). I highly doubt it is the suggested method but my way of solving this was to call a custom MergeChanges method in each action called by my wizard:
public Order MergeChanges(Order newOrder)
{
var sessionHistory = (List<string>)Session["sessionHistory"];
if (sessionHistory == null || sessionHistory.Count == 0)
return MergeChanges(newOrder, -1);
return MergeChanges(newOrder, MasterViewController.GetStepNumberByName(sessionHistory.Last()));
}
public Order MergeChanges(Order newOrder, int step)
{
PreMerge(newOrder);
Order result = null;
try
{
ApplyLookups(ref newOrder);
Order oldOrder = (Order)Session["order"];
if (oldOrder == null)
{
Session["order"] = newOrder;
result = newOrder;
}
else
{
List<TypeHelper.DecoratedProperty<ModelPageAttribute>> props = null;
newOrder.GetType().GetDecoratedProperty<ModelPageAttribute>(ref props);
props = props.Where(p => (p.Attributes.Count() > 0 && p.Attributes.First().PageNumber.Contains(step))).ToList();
foreach (var propPair in props)
{
object oldObj = oldOrder;
object newObj = newOrder;
if (!string.IsNullOrEmpty(propPair.PropertyPath))
{
bool badProp = false;
foreach (string propStr in propPair.PropertyPath.Split('\\'))
{
var prop = oldObj.GetType().GetProperty(propStr);
if (prop == null)
{
badProp = true;
break;
}
oldObj = prop.GetValue(oldObj, BindingFlags.GetProperty, null, null, null);
newObj = prop.GetValue(newObj, BindingFlags.GetProperty, null, null, null);
}
if (badProp)
continue;
}
if (newObj == null)
continue;
var srcVal = propPair.Property.GetValue(newObj, BindingFlags.GetProperty, null, null, null);
var dstVal = propPair.Property.GetValue(oldObj, BindingFlags.GetProperty, null, null, null);
var mergeHelperAttr = propPair.Property.GetAttribute<MergeHelperAttribute>();
if (mergeHelperAttr == null)
{
if (newObj != null)
propPair.Property.SetValue(oldObj, srcVal, BindingFlags.SetProperty, null, null, null);
}
else
{
var mergeHelper = (IMergeHelper)Activator.CreateInstance(mergeHelperAttr.HelperType);
if (mergeHelper == null)
continue;
mergeHelper.Merge(context, HttpContext.Request, newObj, propPair.Property, srcVal, oldObj, propPair.Property, dstVal);
}
}
result = oldOrder;
}
}
finally
{
PostMerge(result);
}
return result;
}
Since my case was doing this with a wizard, only specific values applied to each page so in order to only account for properties known to the current page of the wizard, I've implemented some attributes, a (admittedly over complex) ViewController layer, and a custom validation layer. I can share some more code but the code above does the grunt work if you aren't in such a complex situation. If there is a better way, I hope to learn it from the answers to this question because this was a PITA.
I am surprised this question doesn't come up more often, and I am also surprised the obvious (IMHO) answer isn't standard practice these days: nearly all POSTs should be Ajax-based. This solves a whole slew of problems including
No need to repopulate form data when you have e.g. a validation error, or application error (exception). This is particularly desirable when you have client-side state (in true rich web application fashion).
No compulsion to perform client-side validation. Validation can be 100% server-side (where it must be anyways) and the user experience is nearly the same.
Of course, there is some initial work you need to do to build out a framework for this, for example, I have a set of AjaxUpdate, AjaxNothing, AjaxRedirect, AjaxErrors ... ActionResult types which render Json which is processed by some custom Javascript. But once you get that in place, it's smooth sailing.
Asp .net MVC 3 application...
This is the View:
Grupa: <%= Html.DropDownListFor(x => x.Grupa, Model.ListaGrupe) %>
Produsul: <%= Html.DropDownListFor(x => x.Produs, Model.ListaProduse) %>
Cantitate: <%=Html.TextBoxFor(x => x.Cantitate, new { style = "width: 100px;" })%>
Pret: <%=Html.TextBoxFor(x => x.Pret, new { style = "width: 100px;", disabled = true})%>
TVA: <%= Html.TextBoxFor(x => x.TVA, new { style = "width: 100px;", disabled = true })%>
Valoare: <%= Html.TextBoxFor(x => x.NoTVA, new { style = "width: 120px;", disabled = true})%>
Valoare cu TVA: <%=Html.TextBoxFor(x => x.Total, new { style = "width: 120px;", disabled = true})%>
I am using some JQuery to change Pret, TVA, NoTVA and Total based on the values in Grupa, Produs and Cantitate so I don't want the user to modify the values inside them.
Probably disabled = true shoudn't be used. Then how can I make so the user can't modify the fields but the value to be posted to the controller's action?
You can also make them readonly rather than disabling them. On the other note, I think #Chris solution is better, that way your modified data will be posted back.
You can use Html.HiddenFor() and use a <span> or <div> instead. Their values will then be posted back.
Well, this is what i did up to now,
i didn't succeed to make a good, easy to use, readonly protection using encryption,
but i did manage to do something that i think might just do.
how it works:
When you use LockObject(o) an object, itterate the properties that have defined ProtectedAttribute defined for.
add the locked value to a list, specially made for this field.
! the list is kept in the user session (on the server side)
when the user submits the form, IsValid checks to see if the value is in the list of locked values. if yes, then it is all ok. otherwise, it must have been changed somehow.
! the number of values is not that big, and is temporary to the session, but if it is bothering someone, a simple lockList.remove(node); can easly be added when a value is validated.
Note: this can cause problem when the user uses Back buttons or Resubmit a form using Refresh.
tell me if you find any problems that this model does not take into account...
+ the Equalization is very naive, so it works only with value-types for time be.
Code:
Created an attribute named ProtectedAttribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
public class ProtectedPropertyAttribute : ValidationAttribute
{
private static Dictionary<string, LinkedList<object>> savedValues;
static ProtectedPropertyAttribute()
{
savedValues = (Dictionary<string, LinkedList<object>>)HttpContext.Current.Session["ProtectedAttributeData"];
if (savedValues != null)
return;
savedValues = new Dictionary<string, LinkedList<object>>();
HttpContext.Current.Session.Add("ProtectedAttributeData", savedValues);
}
public static void LockObject(object obj)
{
Type type = obj.GetType();
foreach (PropertyInfo property in type.GetProperties())
{
LockProperty(obj, property);
}
}
public static void LockProperty(object obj, PropertyInfo property)
{
ProtectedPropertyAttribute protectedAttribute =
(ProtectedPropertyAttribute)
property.GetCustomAttributes(typeof (ProtectedPropertyAttribute), false).FirstOrDefault();
if (protectedAttribute == null)
return;
if(protectedAttribute.Identifier == null)
protectedAttribute.Identifier = property.Name;
LinkedList<object> list;
if (!savedValues.TryGetValue(protectedAttribute.Identifier, out list))
{
list = new LinkedList<object>();
savedValues.Add(protectedAttribute.Identifier, list);
}
list.AddLast(property.GetValue(obj, null));
}
public string Identifier { get; set; }
public ProtectedPropertyAttribute()
{
}
public ProtectedPropertyAttribute(string errorMessage) : base(errorMessage)
{
}
public ProtectedPropertyAttribute(Func<string> errorMessageAccessor) : base(errorMessageAccessor)
{
}
protected override ValidationResult IsValid (object value, ValidationContext validationContext)
{
LinkedList<object> lockedValues;
if (Identifier == null)
Identifier = validationContext.DisplayName;
if (!savedValues.TryGetValue(Identifier, out lockedValues))
return new ValidationResult(FormatErrorMessage(validationContext.MemberName), new[] { validationContext.MemberName });
bool found = false;
LinkedListNode<object> node = lockedValues.First;
while (node != null)
{
if(node.Value.Equals(value))
{
found = true;
break;
}
node = node.Next;
}
if(!found)
return new ValidationResult(FormatErrorMessage(validationContext.MemberName), new[] { validationContext.MemberName });
return ValidationResult.Success;
}
}
place this attribute on any property of your model just as any other validation.
public class TestViewModel : Controller
{
[ProtectedProperty("You changed me. you bitch!")]
public string DontChangeMe { get; set; }
public string ChangeMe { get; set; }
}
in the controller, after you are finished with the viewmodel object,
you call ProtectedAttribute.LockObject(myViewModel)
public class TestController : Controller
{
public ActionResult Index()
{
TestViewModel vm = new TestViewModel {ChangeMe = "a1", DontChangeMe = "b1"};
ProtectedPropertyAttribute.LockObject(vm);
return View(vm);
}
public string Submit(TestViewModel vm)
{
string errMessage;
return !validate(out errMessage) ? "you are a baaad, man." + errMessage : "you are o.k";
}
private bool validate(out string errormessage)
{
if (ModelState.IsValid)
{
errormessage = null;
return true;
}
StringBuilder sb = new StringBuilder();
foreach (KeyValuePair<string, ModelState> pair in ModelState)
{
sb.Append(pair.Key);
sb.Append(" : <br/>");
foreach (ModelError err in pair.Value.Errors)
{
sb.Append(" - ");
sb.Append(err.ErrorMessage);
sb.Append("<br/>");
}
sb.Append("<br/>");
}
errormessage = sb.ToString();
return false;
}
}