I am using a viewmodel with required field validation specified for some properties. I was able to create readonly version of same model using "displayfor". In a page, along with this readonly view there are other controls too along with submit. Now, when I click on "submit", it is getting validated and ModelState is invalid. How to remove validation, if we use model only for display.
ViewModel
public class CustomerVM
{
[Required]
public string Name {get;set;}
}
View
#using (Html.BeginForm("CreateCustomer, "Customer", FormMethod.Post))
{
#Html.DisplayFor(o => o.Name)
#..other input controls.#
<input id="btnSave" type="submit" value="Save" />
}
Model.State is invalid since name is rendered as label and httppost doesn't have that value.
This is where MetadataTypeAttribute comes in handy:
public class MyModel
{
public string Name { get; set; }
}
public interface IMyModelValidation
{
[Required]
public string Name { get; set; }
}
[MetadataType(typeof(IMyModelValiation))]
public class MyModelValidation : MyModel { }
Now MyModel has no validation and MyModelValidation does have validation, and they can be used almost interchangeably.
MetadataType
The MetadataTypeAttribute attribute enables you to associate a class with a data-model partial class. In this associated class you provide additional metadata information that is not in the data model.
For example, in the associated class you can apply the RequiredAttribute attribute to a data field. This enforces that a value is provided for the field even if this constraint is not required by the database schema.
You could use a different view model with different validation requirements for this 'read only' view. Or you could use the ModelState.Remove() method in your controller to get rid of errors against properties that you don't want validated. IMO the separate view model approach is better.
edit after seeing your code
Add a hiddenfor
#Html.DisplayFor(o => o.Name)
#Html.HiddenFor(o => o.Name)
That will pass the data back to the controller on the post and result in ModelState.IsValid == true
I'm not saying this is the best approach but I had to do something similar so I setup validation groups. I created an attribute that I placed on each model property that defined its validation group. Then on postback I called an extension method on the ViewDataDictionary and passed in the validation group that I wanted to run validation on. This would remove any validation messages for all other groups. Here is some example code:
The attribute:
/// <summary>
/// Attribute that assigns the property on a model to a given
/// validation group. By using the ValidationGroupExtension
/// and calling ValidateGroup on the ViewData validation errors
/// for properties that are not included in the validation group
/// will be removed.
/// </summary>
public class ValidationGroup : Attribute
{
/// <summary>
/// The unique name of the group.
/// </summary>
public String Group { get; set; }
public ValidationGroup(String group)
{
this.Group = group;
}
}
The extension:
/// <summary>
/// Used in conjunction with the ValidationGroup attribute to
/// specify which fields in a model should be validated. The
/// ValidateGroup extension should be called on ViewData before
/// checking whether the model is valid or not.
/// </summary>
public static class ValidationGroupExtension
{
/// <summary>
/// Remove all validation errors that are assocaited with
/// properties that do not have the ValidationGroup attribute
/// set with the specified group name.
///
/// This only handles flat models.
/// </summary>
/// <param name="viewData">View Data</param>
/// <param name="model">Data model returned</param>
/// <param name="group">Name of the validation group</param>
/// <returns></returns>
public static ViewDataDictionary ValidateGroup(this ViewDataDictionary viewData, Object model, String group)
{
//get all properties that have the validation group attribut set for the given group
var properties = model.GetType().GetProperties()
.Where(x => x.GetCustomAttributes(typeof(ValidationGroup), false)
.Where(a => ((ValidationGroup)a).Group == group).Count() > 0)
.Select(x => x.Name);
//find all properties that don't match these properties
var matchingProperties = viewData.ModelState.Where(x => !properties.Contains(x.Key)).ToList();
//remove any property that isn't in the gorup
foreach (var matchingProperty in matchingProperties)
{
viewData.ModelState.Remove(matchingProperty.Key);
}
return viewData;
}
}
On PostBack:
ViewData.ValidateGroup(model, "my validation group name");
if (ModelState.IsValid)
{
...
}
On the ViewModel:
[Required]
[DisplayName("Name")]
[ValidationGroup("my validation group name")]
public string Name { get; set; }
Related
I have a view which is bound with a ViewModel which contains multiple viewmodels.
Now, the parent view contains views(rendered by #html.partial) each view bound with its corresponding viewmodel and has its own form action.
My Question:
I could view the data correctly, but i can't submit each subview alone, so how can post each submodel alone?
Also, when there would be modelstate errors how can i refer to the correct subview?
Any idea would be appreciated.
Extra info:
The code sample shows what i did exactly:
ViewModel:
public class ViewModelParent
{
public ViewModelChild1 ViewModelC1 {get; set;}
public ViewModelChild2 ViewModelC2 {get; set;}
public ViewModelChild3 ViewModelC3 {get; set;}
}
Controller:
public ActionResult GetParent()
{
return view(new ViewModelParent());
}
Views:
GetParent.cshtml (contains views for each submodel).
#model Models.ViewModelParent
#Html.Partial("~/Views/Children/GetC1.cshtml", Model.ViewModelC1)
#Html.Partial("~/Views/Children/GetC2.cshtml", Model.ViewModelC2)
#Html.Partial("~/Views/Children/GetC3.cshtml", Model.ViewModelC3)
Children views:
GetC1.cshtml
#model ViewModelChild1
<form action="#Url.Action("GetC1", "Child"" method="POST" class="smart-form" id="frm_child1">
#Html.AntiForgeryToken()
#Html.ValidationSummary()
#* controls here*#
</form>
The same applies for the rest children views GetC2.cshtml & GetC3.cshtml
I've done something similar in the past.
I'd recommend this as a possible approach (assuming you want to stick with full page postbacks instead of going the ajax route).
Use your existing Parent ViewModel class (with child Models)
public class ViewModelParent
{
public ViewModelChild1 ViewModelC1 {get; set;}
public ViewModelChild2 ViewModelC2 {get; set;}
public ViewModelChild3 ViewModelC3 {get; set;}
}
Have the partial views each use the Parent Model
#model Models.ViewModelParent
#Html.Partial("~/Views/Children/GetC1.cshtml", Model)
#Html.Partial("~/Views/Children/GetC2.cshtml", Model)
#Html.Partial("~/Views/Children/GetC3.cshtml", Model)
The Child views each have the parent Model, but only contain form elements for the Child Model of that view. If you want a validation summary in every partial view you have to get a bit creative - I'll explain later...
eg: GetC1.cshtml
#model ViewModelParent
#using(Html.BeginForm("GetParent", "ParentControllerName", null, FormMethod.Post, new {#class="smart-form" id="frm_child1"}))
{
#Html.AntiForgeryToken()
#Html.ValidationSummaryForGroup(ViewBag.ChildType, "Child1") #* I'll explain this later *#
#* controls here - eg... *#
#Html.TextBoxFor(m => m.ViewModelChild1.Property1)
}
Then your controller can simply farm out the child methods if the form is valid (or return if not)
Eg:
public class ParentControllerNameController : Controller
{
public ActionResult GetParent()
{
return View(new ViewModelParent());
}
[HttpPost]
public ActionResult GetParent(ViewModelParent model)
{
if (ModelState.IsValid)
{
if (model.ViewModelC1 != null)
{
return GetC1(model.ViewModelC1);
}
else if (model.ViewModelC2 != null)
{
return GetC2(model.ViewModelC2)
}
else if (model.ViewModelC3 != null)
{
return GetC3(model.ViewModelC3)
}
} else {
// invalid!
if (model.ViewModelC1 != null)
{
ViewBag.ChildType = "Child1";
}
else if (model.ViewModelC2 != null)
{
ViewBag.ChildType = "Child2";
}
else if (model.ViewModelC3 != null)
{
ViewBag.ChildType = "Child3";
}
// needed to prevent null reference errors
if (model.ViewModelC1 == null) model.ViewModelC1 = new ViewModelChild1();
if (model.ViewModelC2 == null) model.ViewModelC2 = new ViewModelChild2();
if (model.ViewModelC3 == null) model.ViewModelC3 = new ViewModelChild3();
}
return View(model);
}
}
The above else-if statements will work, because each child view only contains properties for that child model - hence the other child viewmodels are null.
Note I used a new Html Helper extension above that I created that wraps the Validation Summary so you can display errors specific to the child model. A simple display/not display is insufficient because you'd lose client side validation errors being shown otherwise.
Of course this is only necessary when you have a validation summary in every partial view. If there's just one validation summary then you can stick with a simple #Html.ValidationSummary()
namespace System.Web.Mvc.Html
{
public static class ValidationSummaryForGroupExtensions
{
public static MvcHtmlString ValidationSummaryForGroup(this HtmlHelper html, string testValue, string expectedValue)
{
return ValidationSummaryForGroup(html, testValue, expectedValue, false);
}
/// <summary>
/// Displays a validation summary which shows serverside errors only if the specified testvalue and value are equal. Client side validation will work as normal.
/// <para>The purpose of this is to allow multiple valiation summaries (for multiple forms) on a single page.</para>
/// </summary>
/// <param name="testValue">Value to test (could be a value in viewbag)</param>
/// <param name="expectedValue">Value to expect if the server side errors are to be displayed.</param>
/// <returns></returns>
public static MvcHtmlString ValidationSummaryForGroup(this HtmlHelper html, string testValue, string expectedValue, bool excludePropertyErrors)
{
if (testValue != null && testValue.ToLower() == expectedValue.ToLower())
return html.ValidationSummary(excludePropertyErrors);
return new MvcHtmlString("<div class=\"validation-summary-valid\" data-valmsg-summary=\"true\"><ul><li style=\"display:none\"></li></ul></div>");
}
}
}
Of course you could do partial postbacks using ajax - in which case the child views could be directly for the child models, and each child form postback directly to the relevant method in your controller.
This is simple, in the handler of the post method of the controller, call the name of the parameter after the property name in the view model.
So in you view model you have:
public class ViewModelParent
{
public ViewModelChild1 viewModelC1 {get; set;}
public ViewModelChild2 viewModelC2 {get; set;}
public ViewModelChild3 viewModelC3 {get; set;}
}
In the post handler of your controller you will need something like:
<HttpPost()>
Function GetC1(viewModelC1 As ViewModelChild1 ) As ActionResult
in the html all the properties will be names like 'viewModelC1.nameofsomething' and this helps the model binder map the properties up. The above is VB.net but you should get the idea.
hope that helps
Andy
Is there a way to force binding of properties A and B before C?
There's Order property in the System.ComponentModel.DataAnnotations.DisplayAttribute class, but does it affect binding order?
What i'm trying to achieve is
page.Path = page.Parent.Path + "/" + page.Slug
in a custom ModelBinder
Why not implement the Page property as:
public string Path{
get { return string.Format("{0}/{1}", Parent.Path, Slug); }
}
?
I would have initially recommended Sams answer as it would have not involved any binding of the Path property at all. You mentioned that you could concatenate the values using a Path property as this would cause lazy loading to occur. I imagine therefore you are using your domain models to display information to the view. I would therefore recommend using view models to display only the information required in the view (then use Sams answer to retrieve the path) and then map the view model to the domain model using a tool (i.e. AutoMapper).
However, if you continue to use your existing model in the view and you cannot use the other values in the model, you can set the path property to the values provided by the form value provider in a custom model binder after the other binding has occurred (assuming no validation is to be performed on the path property).
So lets assume you have the following view:
#using (Html.BeginForm())
{
<p>Parent Path: #Html.EditorFor(m => m.ParentPath)</p>
<p>Slug: #Html.EditorFor(m => m.Slug)</p>
<input type="submit" value="submit" />
}
And the following view model (or domain model as the case may be):
public class IndexViewModel
{
public string ParentPath { get; set; }
public string Slug { get; set; }
public string Path { get; set; }
}
You can then specify the following model binder:
public class IndexViewModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//Note: Model binding of the other values will have already occurred when this method is called.
string parentPath = bindingContext.ValueProvider.GetValue("ParentPath").AttemptedValue;
string slug = bindingContext.ValueProvider.GetValue("Slug").AttemptedValue;
if (!string.IsNullOrEmpty(parentPath) && !string.IsNullOrEmpty(slug))
{
IndexViewModel model = (IndexViewModel)bindingContext.Model;
model.Path = bindingContext.ValueProvider.GetValue("ParentPath").AttemptedValue + "/" + bindingContext.ValueProvider.GetValue("Slug").AttemptedValue;
}
}
}
And finally specify that this model binder is to be used by using the following attribute on the view model:
[ModelBinder(typeof(IndexViewModelBinder))]
I have some fields in my ViewModel that I only need for the output in my View. But they are always getting send when submitting the form. That's not only annoying, but also dangerous, because I definitly don't want that databinding for these viewmodel fields.
Are there any change to define a OneWay Databinding for some of my viewmodel properties?
Thx in advance
EDIT:
The problem ist, that the Pictures and ValidSizes List is send back to the server when I click to an ActionLink.
http://localhost:52176/?PageSize=30&Index=31&Pictures=System.Collections.Generic.List%601[System.String]&Size=100&ValidSizes=System.Collections.Generic.List%601[System.Web.Mvc.SelectListItem]
public class PicturesViewModel
{
public const int SMALL = 100;
public const int MIDDLE = 150;
public const int BIG = 250;
public int PageSize { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PicturesViewModel"/> class.
/// </summary>
public PicturesViewModel()
{
Pictures = new List<string>();
Size = SMALL;
Index = 1;
PageSize = 30;
}
/// <summary> Gets or sets the index. </summary>
public int Index { get; set; }
/// <summary>
/// Gets or sets the picture links.
/// </summary>
/// <value>The picture links.</value>
public List<string> Pictures { get; private set; }
/// <summary>
/// Gets or sets the size.
/// </summary>
/// <value>The size.</value>
public int Size { get; set; }
private List<SelectListItem> validSizes = null;
/// <summary>
/// Gets the valid sizes.
/// </summary>
/// <value>The valid sizes.</value>
public IEnumerable<SelectListItem> ValidSizes
{
get {
if (validSizes != null)
return validSizes;
validSizes = new List<SelectListItem>
{
new SelectListItem(){Text = "Small", Value = SMALL.ToString()},
new SelectListItem(){Text = "Middle", Value = MIDDLE.ToString()},
new SelectListItem(){Text = "Big", Value = BIG.ToString()}
};
return validSizes;
}
}
}
EDIT2:
<div id="pager_left">
<%= Html.ActionLink("Prev", "Prev", Model)%>
</div></td>
That's the Action Link that causes the binding.
Use separate ViewModel for form input
Use IEnumerable instead of List
Use [Bind(Exclude="Pictures, ValidSizes")] on the action input parameter
Use private setters
Don't create form input elements for Pictures/ValidSizes if you don't need them
and so on.
Update:
You assign different ViewModel not to a view, but to the controller action that handles link click. And from your questions it seems that what you need is not "oneway" binding, but rather avoiding extra characters in URL - because, if your ValidSizes is IEnumerable it won't be altered, and anyway in your URL its data is wrong, won't cause update - so it's already "one-way" binding.
This is what I can find for your problem:
http://forums.asp.net/t/1328683.aspx
As for solution, I never use ActionLink helper myself, because it is a leaky abstraction and I don't like to fight with it. Simple html link tag is always much better.
<a href="<%= Html.BuildUrlFromExpression<>() %>" />
I actually use my own few-lines version of the BuildUrlFromExpression for this. Also see in the link above how you can pass parameters via anonymous object instead of Model (new { PageSize = Model.PageSize, index = Model.index }).
Currently I have a DataModel object which contains my linq to sql classes(a dmbl file). Currently I use a partial class to validate the incoming input. For example
public partial class User : IEntity
{
public NameValueCollection CheckModel()
{
return GetRuleViolations();
}
/// <summary>
/// Method validates incoming data, by given rules in the if statement.
/// </summary>
/// <returns>NameValueCollection</returns>
private NameValueCollection GetRuleViolations()
{
NameValueCollection errors = new NameValueCollection();
if (string.IsNullOrEmpty(Username))
errors.Add("Username", "A username is required");
// and so on
return errors;
}
}
Now what I want to try to do is add validation attributes to the fields. For example I want to try to add the required attribute to the field Username instead/in addtion of using the validation I currently have. My question is how can I achieve this because the dmbl file is auto generated. Or maybe it is not possible and should I use a different approach?
You should read about Metadata classes. This is example blog entry about it.
Adding Required atrribute to User class will be something like:
[MetadataType(typeof(UserMetadata))]
public partial class User
{
}
public class UserMetadata
{
[Required]
public string Username { get; set; }
}
Is there a way to get a reference for the ViewPage that contains a parital view from the partial view ??
Not 100% but I don't think this is possible. What specifically do you want to reference in the ViewPage from the Partial?
Couldn't you just share a Model between the ViewPage and ViewUserControl?
It seems there is no standard property for this so you should pass ViewPage object to partial view yourself:
<% Html.RenderPartial("partial_view_name", this); %>
The absolute answer: NO
You need to use ViewData or Model to share it.
My solution was a baseclass for any models used by partial controls.
It's useful for the times when you need to specify a model but want the partial view to have access to certain things from the containing View's model.
Note: this solution will support a hierarchy of partial views automatically.
Usage:
When you call RenderPartial supply the Model (for the view).
Personally I prefer this pattern, which is to create a view in place on the page consisting of whatever the patial view might need from the parent model.
I create a ProductListModel from the current model, which makes the parent model available easily to the partial view.
<% Html.RenderPartial("ProductList", new ProductListModel(Model)
{ Products = Model.FilterProducts(category) }); %>
In the partial control itself you specify the ProductListModel as a strongly typed view.
<%# Control Language="C#" CodeBehind="ProductList.ascx.cs"
Inherits="System.Web.Mvc.ViewUserControl<ProductListModel>" %>
Model class for the partial view
Note: I'm using IShoppingCartModel to specify the model to avoid coupling from the partial back to the containing view.
public class ProductListModel : ShoppingCartUserControlModel
{
public ProductListModel(IShoppingCartModel parentModel)
: base(parentModel)
{
}
// model data
public IEnumerable<Product> Products { get; set; }
}
Baseclasses:
namespace RR_MVC.Models
{
/// <summary>
/// Generic model for user controls that exposes 'ParentModel' to the model of the ViewUserControl
/// </summary>
/// <typeparam name="T"></typeparam>
public class ViewUserControlModel<T>
{
public ViewUserControlModel(T parentModel)
: base()
{
ParentModel = parentModel;
}
/// <summary>
/// Reference to parent model
/// </summary>
public T ParentModel { get; private set; }
}
/// <summary>
/// Specific model for a ViewUserControl used in the 'store' area of the MVC project
/// Exposes a 'ShoppingCart' property to the user control that is controlled by the
/// parent view's model
/// </summary>
public class ShoppingCartUserControlModel : ViewUserControlModel<IShoppingCartModel>
{
public ShoppingCartUserControlModel(IShoppingCartModel parentModel) : base(parentModel)
{
}
/// <shes reummary>
/// Get shopping cart from parent page model.
/// This is a convenience helper property which justifies the creation of this class!
/// </summary>
public ShoppingCart ShoppingCart
{
get
{
return ParentModel.ShoppingCart;
}
}
}
}