I have an add product form which depends upon the user having added categories, product types etc. previously. Rather than let them complete the form, and then realise that they can't submit it, I want to show an error when the form first loads.
I want to add this error to the modelstate against the appropriate property - so my question is how can I get the modelstate key name for that property?
Code
Model.Step1.PopulateData(_productTypeSvc.GetList(), _websiteSvc.GetMaximumNumberOfProductImages(HttpContext.Request.Url.Host));
Model.Title = "Add A Product - Step 1: The Basics";
if (Model.Step1.ProductTypes.IsNullOrEmpty())
{
//Rather than string.Empty, I want to programmatically get the correct key to add the error to..
//E.g. if it were the view - HtmlHelper.NameFor(m => m.Step1.ProductTypeID)
ModelState.AddModelError(string.Empty, "Please add at least one product type before adding products.");
}
I know I can dummy the HtmlHelper object, but I'd rather avoid this and wondered if there is a better way?
Found an appropriate extension in MVC:
System.Web.Mvc.ExpressionHelper.GetExpressionText(LambdaExpression expression);
MSDN Here.
Thanks anyway guys.
Update:
public static string GetPropertyName<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> propertySelector)
{
return ExpressionHelper.GetExpressionText(propertySelector);
}
Then use the extension:
ModelState.AddModelError(Model.GetPropertyName(m => m.Step1.ProductTypeID), "Please add at least one product type before adding products.");
Related
I am trying to build a object/model in memory that is a representation of what the model would look like if the current ModelState was assumed to be correct and was Merged into the model.
I thought I would look into the source code for TryUpdateModel() or UpdateModel() and came up with the following:
private T GetTempModel<T>(T model, ActionExecutedContext filterContext)
{
var mtype = Type.GetType(model.GetType().FullName);
var m = Activator.CreateInstance(mtype);
var binder = ModelBinders.Binders.GetBinder(mtype);
var bc = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => m, mtype),
ModelName = null,
ModelState = filterContext.Controller.ViewData.ModelState,
ValueProvider = filterContext.Controller.ValueProvider
};
var result = binder.BindModel(filterContext.Controller.ControllerContext, bc);
return (T)result;
}
However when I do this the result value is just the default state that was created when I ran the CreateInstance(). It didn't merge all the proposed values in the ModelState.
Am I misunderstanding how this should work? Is there a way to get a model representation of the items in ModelState in object form instead of KeyValuePair?
UPDATE: Additional information per comments.
I am simplifying this a bit but here is the basic scenario for what I am doing. Let's say I have a survey application. The user logs in and is presented with a basic View where they enter their profile information (first, last name etc.) The viewModel used to generate this view has a property, SurveyQuestions, that is initialized with no questions. As they navigate through the next screens they are prompted with questions, these questions are loaded into the View/DOM via AJAX for performance reasons as they answer one question I go grab the next applicable one.
When the user is complete and POSTs the survey I get everything as expected in the ViewModel and in SurveyQuestions I can see all their questions/answer information (List<QuestionBlock>).
However I am using the Post-Redirect-Get (PRG) pattern and if there is an exception during the POST I store the ModelState, redirect, and reapply the ModelState upon exiting the GET. The problem is that in the GET the ViewModel is just the uninitialized and empty question SurveyQuestions property. Because of this the ModelState values aren't initialized properly for the SurveyQuestions when the view is rendered. All other properties get the correct ModelState values when the view is rendered.
This is because I have to initialize the user's SurveyQuestions to have each of the question information before exiting the GET.
This is where I would like to read the values from the ModelState into a temporary object so that I can access them and rebuild those questions server side to correctly rendered in the view.
#model Customer
#Html.Partial("_UserProfile", (UserProfile)Model.UserProfile)
When i run this code, i get this error:
The model item passed into the dictionary is of type 'Customer', but this dictionary requires a model item of type 'UserProfile'.
Partial View _UserProfile is strongly typed.
I want to be able to edit these field.
Any suggestions?
Make sure your Model.UserProfile is not null.
I found your post trying to debug the same error, and it turned out I hadn't initialised my "Model.UserProfile" equivalent.
I guess what's happening here, is that if a null model is passed to RenderPartial, it defaults to using the main view's model? Can anyone confirm this?
If Model.UserProfile is null, it will attempt to pass in your customer model.
Two ways to get around this:
#model Customer
#Html.Partial("_UserProfile", (UserProfile)Model.UserProfile, new ViewDataDictionary())
Or:
#model Customer
if (Model.UserProfile != null)
{
#Html.Partial("_UserProfile", (UserProfile)Model.UserProfile)
}
I ran into this problem when dealing with parts of a user profile such as Name and Address records. If the user had an incomplete profile I want the account management view to detect a null Address record and display an Action link to create a new Address or display whatever address data is available.
As described by others when null is passed the overload for Html.RenderPartial gets triggered and the parent View Model is passed through. I ended up converting my partial views to Display and Editor Templates to get around it. Here are some How-To articles from: Hansleman and codeguru
You get better re-usability from this method and it preserves the null values:
In your View:
#Html.DisplayFor( m=> m.Address)
Then handle the null value in the DisplayTemplate.
#model Namespace.Models.MyObject
...
if(#Model != null){
...
}else{
...
}
I have faced the same problem but finally I had figured it out.
There is a type mismatch in passed models .. Your View accepts model of type Customer but you partial view is passing the model Userprofile so what you have to do is pass the same model in both or.... create a model that have all properties of both models. Surely your problem will be solved.
It will fallback on initial model if passed item is null.
Try this:
#Html.Partial("_UserProfile", (UserProfile)Model.UserProfile ?? new UserProfile())
Your trying to case a Customer type object to a UserProfile type object. By default this wont work as the framework has no idea how to cast these objects. If you absolutely must do it this way the only option is to provide explicit cast operator like:
public static explicit operator Digit(byte b) // explicit byte to digit conversion operator
{
Digit d = new Digit(b); // explicit conversion
System.Console.WriteLine("Conversion occurred.");
return d;
}
You can read more about it here.
Add the keyword "virtual" to the UserProfile property on the Customer model.
It is the easyest way overcome the lazy loading, but performance..
I'm using #Html.Password("Password", ViewData["password"]) for displaying password in popup, the password is being displayed and I should be able to edit and update it. After editing it, when I click save button password is not being updated.
ModelState.IsValid is giving false and the method is being skipped.
From Password Method signature
public static string Password(
this HtmlHelper htmlHelper,
string name
)
second argument is
The name of the form field and the ViewDataDictionary key that is used to look up the value.
So it is practically wrong to pass ViewData["password"] as second argument.
MVC will automatically look for value into ViewData, so you could just write
#Html.Password("Password")
Or do everything correct way and use strongly typed view models
I am trying to work around the fact that when they wrote asp.net MVC 3 they forgot to include code to add the unobtrusive validation attributes to select lists and their "fix" for this is to include it in MVC 4, which is no bloody use to anyone using MVC 3.
My proposed work around is to use Html.GetUnobtrusiveValidationAttributes() to add them myself, just like any other custom attributes, but i can't work out the correct syntax for calling the method. There are 2 overloads, one takes a string and the other takes a string and a ModelMetaData class. I understand the metadata param, I presume I just pass in ViewData.ModelMetadata but what should the string be? The MSDN documentation says it is "the specified HTML name attribute" which makes no sense to me. The HTML name attribute of what? The select list? Why would it need that and how does that help it know what property on my model i want the validation for? Looking at examples of usage they all seem to pass in the name of the property on my model that i want the validation attributes for, which makes sense. Unfortunately I can't get the method to return anything but an empty collection no matter what i pass in.
My model class is called Event and my property is called EventTypeID. I am using a slightly different viewmodel class as the basis for the view because i need to display a list of Events and also also allow a new event to be entered on the same view so i have a simple viewmodel class as below:
public class EventViewModel
{
public Model.Event NewEvent { get; set; }
public IEnumerable<Model.Event> Events { get; set; }
}
The dropdown list is mapped to the property like: #Html.DropDownListFor(model => model.NewEvent.EventTypeID what do I pass as the string to Html.GetUnobtrusiveValidationAttributes(string) or Html.GetUnobtrusiveValidationAttributes(string, ModelMetadata) to get the attributes for this property. I have tried:
Html.GetUnobtrusiveValidationAttributes("EventTypeID")
Html.GetUnobtrusiveValidationAttributes("EventTypeID",ViewData.ModelMetadata)
Html.GetUnobtrusiveValidationAttributes("NewEvent.EventTypeID")
Html.GetUnobtrusiveValidationAttributes("NewEvent.EventTypeID",ModelMetadata)
They all return an empty collection.
I know that my model is correct because if i change the call from Html.DropDownListFor to Html.TextBoxFor then the validation "just works" without me having to do anything other than add the validation attributes to my model class.
EDIT:
Just tried turning client side validation off, the validation works fine server side for all select lists.
For those still looking for an answer, this works for me:
public static IDictionary<string, object> UnobtrusiveValidationAttributesFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> propertyExpression)
{
var propertyName = html.NameFor(propertyExpression).ToString();
var metadata = ModelMetadata.FromLambdaExpression(propertyExpression, html.ViewData);
var attributes = html.GetUnobtrusiveValidationAttributes(propertyName, metadata);
return attributes;
}
Note that I'm using .Net MVC 4, you don't have the html.NameFor method in MVC 3. However, I believe this can be done in MVC 3 with the following method:
var propertyName = ExpressionHelper.GetExpressionText(propertyExpression);
You can use it inline
Example for select element
<select name="#Html.NameFor(m=> m.MyProperty)"
id="#Html.IdFor(m=> m.MyProperty)"
#Html.Raw(string.Join(" ", Html.GetUnobtrusiveValidationAttributes(Html.NameFor(m => m.MyProperty).ToString()).Select(x => x.Key.ToString() + "=\"" + x.Value + "\"")))
>
Here is a link to an answer I posted, showing an HtmlHelper I wrote to provide unobtrusive validation for dropdownlists: MVC 3 dropdownlist validation not working for complex view model
UPDATE
Are you trying to get the attributes in an HtmlHelper, or in-line in your view?
Assuming you are trying to get the attributes in your view, that is the problem.
First, you need to understand that ModelMetadata does not represent a single object available across your entire model. Rather, it represents the metadata for a particular element, be it your model, or any property within the model. A better descriptive name would be ObjectMetadata, since ModelMetadata is the metadata for a specified object, be it a model, a nested model, or a specific property.
ModelMetadata in the view is only the metadata for the top-level model. You must get the ModelMetadata for the property to which the dropdownlist is bound. If you use a helper, then the helper is passed the correct ModelMetadata as a matter of course. If you use your view, you need to engage in some gymnastics to get the correct ModelMetadata, see for example my answer here: Validating and editing a “Changeable”/optional type in Asp.net MVC 3
I am looking for a simple solution to the following problem:
I am using a simple TextBox control with the Text property bound to a property in the code behind. Additionally I am using a validation rule to notify the user of malformed input.
... error display style here ...
Now after entering valid data into the TextBox the user can hit a button to send the data. When clicking the button the data from the bound property UserName in the code behind is evaluated and sent.
The problem is that a user can enter valid data into the TextBox and this will be set in the property UserName. If the user then decides to change the text in the TextBox and the data becomes invalid, the setter of the property UserName is not called after the failed validation.
This means that the last valid data remains in the property UserName, while the TextBox display the invalid data with the error indicator. If the user then clicks on the button to send the data, the last valid data will be sent instead of the current TextBox content.
I know I could deactivate the button if the data is invalid and in fact I do, but the method is called in the setter of UserName. And if that is not called after a failed validation the button stays enabled.
So the question is: How do I enable calling of the property setter after a failed validation?
You could set the ValidationRule.ValidationStep property for your validation rules to ValidationStep.UpdatedValue. This first updates the source, and then performs validation. That means, your property setter should be called even though your validation fails. Note that this property is only available from .NET 3.5 SP1 upwards. For more details, see this blog post (paragraph "How do I use it? (Part 1)").
How I handle this in my view model classes:
public class MyViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private Dictionary<string, string> _Errors = new Dictionary<string, string>();
public object SomeProperty
{
get { return _SomeProperty; }
set
{
if (value != _SomeProperty && !ValidationError("SomeProperty", value))
_SomeProperty = value;
OnPropertyChanged("SomeProperty");
}
}
}
private bool ValidationError(string propertyName, object value)
{
// I usually have a Dictionary<string, Func<object, string>> that maps property
// names to validation functions; the functions return null if the property
// is valid and an error message if not. You can embed the validation logic
// in the property setters, of course, but breaking them out as separate methods
// eases testing.
_Errors[propertyName] = _ValidationMethods[propertyName](value);
OnPropertyChanged("IsValid");
}
public bool IsValid
{
get { return !(_Errors.Where(x => x.Value != null).Any()));
}
public string this[string propertyName]
{
get
{
return (_Errors.ContainsKey(propertyName))
? _Errors[propertyName]
: null;
}
}
}
It's a little awkward to get this all set up at first, but once you've done it, you have a simple and straightforward way to report validation errors to the UI (via the DataErrorValidationRule), a straightforward way to know whether any given property is valid or not (check _Errors), and an IsValid property that tells you whether or not the whole view model is valid. (Also, you can extend the IsValid property to handle the case where all the properties of the view model are valid but the view model itself is not, e.g. two mutually exclusive flags are both set.) And as long as you make them internal, the validation methods can be unit tested via NUnit or whatever.
I should add that the above code is off the top of my head and may or may not work as written - my actual working code is in a base class and has a lot of other things baked into it that would just be confusing.