Custom ValidationAttribute not firing IsValid function call in view model - c#

I have created my own custom ValidationAttribute:
public class UrlValidationAttribute : ValidationAttribute
{
public UrlValidationAttribute() {}
public override bool IsValid(object value)
{
if (value == null)
return true;
var text = value as string;
Uri uri;
return (!string.IsNullOrWhiteSpace(text) &&
Uri.TryCreate(text, UriKind.Absolute, out uri));
}
}
I am using that on one of my models and it works perfectly. However, now I am attempting to use it on a view model:
public class DeviceAttribute
{
public DeviceAttribute(int id, attributeDefinition, String url)
{
ID = id;
Url = url;
}
public int ID { get; set; }
[UrlValidation]
public String Url { get; set; }
}
The view model is used in the partial view like this:
#model List<ICMDB.Models.DeviceAttribute>
<table class="editor-table">
#foreach (var attribute in Model)
{
<tr>
#Html.HiddenFor(a => attribute.ID)
<td class="editor-label">
#Html.LabelFor(a => attribute.Url)
</td>
<td class="editor-field">
#Html.TextBoxFor(a => attribute.Url)
#Html.ValidationMessageFor(a => attribute.Url)
</td>
</tr>
}
</table>
For some unknown reason, while the constructor for UrlValidationAttribute fires, the IsValid function doesn't fire. Any ideas?
Edit: On further investigation, it seems this is happening because the DeviceAttribute view model is actually the view model for a partial. The full page is passed a different view model that contains the list of DeviceAttribute view models. So when my controller action is called, the full page view model is constructed and its values filled, but no DeviceAttribute view models are constructed, hence why no validation is run.

I would recommend you using editor templates instead of writing foreach loops. I suppose that your main view model looks something like this:
public class MyViewModel
{
public List<DeviceAttribute> Devices { get; set; }
...
}
Now in your main view:
#model MyViewModel
#using (Html.BeginForm())
{
<table class="editor-table">
#Html.EditorFor(x => x.Devices)
</table>
<input type="submit" value="OK" />
}
and in the corresponding editor template (~/Views/Shared/EditorTemplates/DeviceAttribute.cshtml):
#model DeviceAttribute
<tr>
#Html.HiddenFor(x => x.ID)
<td class="editor-label">
#Html.LabelFor(x => x.Url)
</td>
<td class="editor-field">
#Html.TextBoxFor(x => x.Url)
#Html.ValidationMessageFor(x => x.Url)
</td>
</tr>
And your POST action takes the view model back:
[HttpPost]
public ActionResult Index(MyViewModel model)
{
...
}
Now the default model binder will successfully bind all values in the view model and kick validation.
Here's a nice blog post about templates.

Related

IEnumerable Error on View with Partial Load

I am trying to create a view in my application that performs basic CRUD commands in ASP.NET Core to teach myself some new skills. I am however stuck and would appreciate some assistance please.
I would like to have each "component" of the application sitting in a partial view for maintenance going forward. I initially had my Index view use a declaration of type IEnumerable (for the for each loop):
#model IEnumerable<Project.Web.Models.Sample.SampleModel>
Which worked perfect for returning the list and rendering the page but then when trying to have my Modal window partially loaded into the page and insert data using the "CreateSample" function on the controller it was not picking up the function and failed the insert (no form action found). If I then try to add:
#model Project.Web.Models.Sample.SampleModel
to the CreateModal view page it throws an error and wont even let me render the page, I presume because its being partial loaded the app is seen as having two SampleModel declarations. If I create this page completely separate and not partially loaded with the normal #model declaration it works.
I have the basic setup going so far and have included my code for each below.
Model - SampleModel
public class SampleModel
{
public int Id { get; set; }
public string SampleText { get; set; }
}
Controller - SampleController
public class SampleController : Controller
{
public const string ControllerName = "Sample";
//Open Database Connection
private _DBContext DBDatabase = new _DBContext ();
public ActionResult Index()
{
var Model = DBDatabase.Sample.Select(s => new SampleModel
{
Id = s.Id,
SampleText = s.SampleText
}).ToList();
return PartialView(Model);
}
[ActionName("_CreateModal")]
public ActionResult InsertNewRecord()
{
var Model = DBDatabase.Sample.Select(s => new SampleModel
{
Id = s.Id,
SampleText = s.SampleText
}).ToList();
return PartialView("_CreateModal", Model);
}
Views - Index, View, Create
Index - Calls Partial Views for View and Create
#using Project.Web.Controllers
#model Project.Web.Models.Sample.SampleModel
<!--html stuff here -->
#await Html.PartialAsync("_CreateModal")
<!--more html stuff here -->
#await Html.PartialAsync("_ViewData")
View - Foreach to Loop Records
#model Project.Web.Models.Sample.SampleModel
<table style="width: 100%;" id="example">
<thead>
<tr>
<th>#</th>
<th>Sample Text</th>
<th class="text-center">Status</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
#foreach (var sample in Model)
{
<tr>
<th scope="row">#sample.Id</th>
<td>#sample.SampleText</td>
<td class="text-center">
<div class="badge badge-success">Active</div>
</td>
<td class="text-center">
<div role="group" class="btn-group-sm btn-group">
<button class="btn-shadow btn btn-primary">Edit</button>
<button class="btn-shadow btn btn-primary">Delete</button>
</div>
</td>
</tr>
}
</tbody>
</table>
Create - Insert New Record
#model Project.Web.Models.Sample.SampleModel
<form method="post" asp-action="/SampleModel/CreateSample">
<div class="form-group">
<label for="CreationTime">SampleText</label>
<div>
<input type="text" class="form-control" id="SampleText" name="SampleText" placeholder="SampleText">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Sign up</button>
</div>
</form>
As per Ammar's comment, you've just copy-pasted the Index Controller's data access. When building a form allowing the user to create a single new item, then the pattern is to typically pre-instantiate an empty model and pass it to the view:
[ActionName("_CreateModal")]
public ActionResult InsertNewRecord()
{
var model = new SampleModel(); // If Id is a GUID, then you could assign one here
return PartialView("_CreateModal", model);
}

Using checkbox for multiple deletion in asp-mvc [duplicate]

I have a view with a table that displays my model items. I've extracted the relevant portions of my view:
#model System.Collections.Generic.IEnumerable<Provision>
#using (Html.BeginForm("SaveAndSend", "Provision", FormMethod.Post))
{
if (Model != null && Model.Any())
{
<table class="table table-striped table-hover table-bordered table-condensed">
<tr>
...
// other column headers
...
<th>
#Html.DisplayNameFor(model => model.IncludeProvision)
</th>
...
// other column headers
...
</tr>
#foreach (var item in Model)
{
<tr>
...
// other columns
...
<td>
#Html.CheckBoxFor(modelItem => item.IncludeProvision)
</td>
...
// other columns
...
</tr>
}
</table>
<button id="save" class="btn btn-success" type="submit">Save + Send</button>
}
...
}
This works fine and the checkbox values are displayed correctly in the view depending on the boolean value of the IncludeProvision field for the given model item.
As per Andrew Orlov's answer, I've modified the view and controller and the SaveAndSend() controller method is now:
[HttpPost]
public ActionResult SaveAndSend(List<Provision> provisions)
{
if (ModelState.IsValid)
{
// perform all the save and send functions
_provisionHelper.SaveAndSend(provisions);
}
return RedirectToAction("Index");
}
However, at this point the passed in model object is null.
Including the Provision model object for completeness:
namespace
{
public partial class Provision
{
...
// other fields
...
public bool IncludeProvision { get; set; }
}
}
My question is, what is the best way to grab the checked/unchecked value from each checkbox and update the session IncludeProvision field for each model item when the 'SaveAndSend' button is clicked?
You cannot use a foreach loop to generate form controls for properties in a collection. It creates duplicate name attributes (in your case name="item.IncludeProvision") which have no relationship to your model and duplicate id attributes which is invalid html. Use either a for loop (you models needs to be IList<Provision>
for(int i = 0; i < Model.Count; i++)
{
<tr>
<td>....</td>
<td>#Html.CheckBoxFor(m => m[i].IncludeProvision)<td>
</tr>
}
or create an EditorTemplate for typeof Provision. In /Views/Shared/EditorTemplates/Provision.cshtml (note the name of the template must match the name of the type)
#model Provision
<tr>
<td>....</td>
<td>#Html.CheckBoxFor(m => m.IncludeProvision)<td>
</tr>
and in the main view (the model can be IEnumerable<Provision>)
<table>
#Html.EditorFor(m => m)
</table>
As #mattytommo said in comments, you should post your model to controller. It can be done with putting your checkbox inside a form. After clicking on button "Save and exit" all data from inputs inside this form will be serialized and sent to your controller where you can perform manipulations with session variables and so on. After that you can redirect wherever you like.
Model
public class YourModel
{
...
public bool IncludeProvision { get; set; }
...
}
View
#model YourModel
...
#using (Html.BeginForm("SaveAndSend", "Test", FormMethod.Post))
{
...
#Html.CheckBoxFor(model => model.IncludeProvision)
...
<button type="submit">Save and send</button>
}
...
Controller
public class TestController : Controller
{
...
[HttpPost]
public ActionResult SaveAndSend(YourModel model)
{
if (ModelState.IsValid)
{
// Some magic with your data
return RedirectToAction(...);
}
return View(model); // As an example
}
...
}

ASP MVC Model List<object> Returned Empty in POST action

I have got this problem that I am having a difficulty to solve. I am creating a page where the user will be presented with a list of items (Product Types). Each item will have a dropdown list next to it so that the user can make appropriate selection to create a mapping. After making selection then the user submits the form, and the value will be written to the database.
The problem is that when it is submitted, I am not getting any values back. Specifically, 'Mappings' is empty in the model that is returned by the POST action. The GET action works fine. The following is the essence of what I have written:
Model:
public class ProductTypeMappingViewModel
{
//this is empty in the POST object
public List<ProductTypeMapping> Mappings { get; set; }
public ProductTypeMappingViewModel()
{
Mappings = new List<ProductTypeMapping>();
}
public ProductTypeMappingViewModel(string db)
{
//use this to populate 'Mappings' for GET action
//works fine
}
public void UpdateDB()
{
//to be called on the object
//returned from POST action
foreach(var mapping in Mappings)
{
//Mappings is always empty after POST
//Suppose to add to db
}
}
}
public class ProductTypeMapping
{
public string ProductTypeName { get; set; }
public int SelectedStandardProductTypeKey { get; set; }
public SelectList StandardProductTypes { get; set; }
public ProductTypeMapping()
{
StandardProductTypes = new SelectList(new List<SelectListItem>());
}
public int GetSelectedProductTypeKey() { //return selected key}
public string GetSelectedProductTypeName() { //return selected name}
}
View:
#model CorporateM10.Models.ProductTypeMappingViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true)
<table class="table">
#foreach (var dept in Model.Mappings)
{
<tr>
<td>
#Html.DisplayFor(model => dept.ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
<div>
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
}
Any insight will be greatly appreciated.
foreach here causes select element in final HTML to have incorrect name attribute. Thus nothing is posted to the server. Replace this with for loop:
<table class="table">
#for (int i=0; i<Model.Mappings.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(model => model.Mappings[i].ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey, model.Mappings[i].StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
As #Andrei said the problem relies on the name attribute.
But to add a little bit to his answer, here's the parameter names in the request that the default model binder expects for your case.
Mappings[0].SelectedStandardProductTypeKey
Mappings[1].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
...
Without any breaks in the numbering, i.e.:
Mappings[0].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
Won't work because of the missing Mapping[1]...
When you use the dropdown helper like this:
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
It generates an input with name="SelectedStandardProductTypeKey" (you need it to be Mappings[0].SelectedStandardProductTypeKey)
If you use a for loop and use the dropdown helper like this:
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey
You'll get the input with the correct name.
Any parameter in the request for which the model binder cannot find a property in the model, it will ignore, that's why the Mappings property is null in your case.
Here are two great resource that explain all this (and that provide alternative ways to represent collections that might be useful if you can't a the for loop to generate a numbered index without breaks):
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

Unable to Bind MVC Model on POST

I'm writing a view that displays a list of managers. The managers have checkboxes next to their name to select them to be removed from the manager list. I am having problems binding the form submission back to my view model. Here's what the page looks like:
Here's the ViewModel for the page.
public class AddListManagersViewModel
{
public List<DeleteableManagerViewModel> CurrentManagers;
}
And here's the sub-ViewModel for each of the DeleteableManagers:
public class DeleteableManagerViewModel
{
public string ExtId { get; set; }
public string DisplayName { get; set; }
public bool ToBeDeleted { get; set; }
}
This is the code for the main View:
#model MyApp.UI.ViewModels.Admin.AddListManagersViewModel
<div class="row">
<div class="span7">
#using (Html.BeginForm("RemoveManagers","Admin"))
{
#Html.AntiForgeryToken()
<fieldset>
<legend>System Managers</legend>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model.CurrentManagers)
</tbody>
</table>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Delete Selected</button>
</div>
}
</div>
</div>
And this is the EditorTemplate I've created for DeleteableManagerViewModel:
#model MyApp.UI.ViewModels.Admin.DeleteableManagerViewModel
<tr>
<td>#Html.DisplayFor(model => model.DisplayName)</td>
<td>
#Html.CheckBoxFor(model => model.ToBeDeleted)
#Html.HiddenFor(model => model.ExtId)
</td>
</tr>
But when I submit the form to the controller the model comes back null! this is what I want it to do:
[HttpPost]
public virtual RedirectToRouteResult RemoveManagers(AddListManagersViewModel model)
{
foreach (var man in model.CurrentManagers)
{
if (man.ToBeDeleted)
{
db.Delete(man.ExtId);
}
}
return RedirectToAction("AddListManagers");
}
I tried following along this post: CheckBoxList multiple selections: difficulty in model bind back but I must be missing something....
Thanks for your help!
Hmm. I think this is ultimately the problem; here's what you're posing:
CurrentManagers[0].ToB‌​eDeleted=true&CurrentManagers[0].ToBeDeleted=false&CurrentManagers[0].Ext‌​Id=X00405982144
Your model is an AddListManagersViewModel that has a collection of CurrentManagers. So, you're posting an array of DeleteableManagerViewModel, which isn't getting bound to the "wrapper" model. You can try changing the model parameter to
params DeleteableManagerViewModel[] model
I don't ever use the EditorFor extensions, though, so I'm just guessing...

How do I map checkboxes onto MVC model members?

I have an MVC view
<%# Page Language="C#" MasterPageFile="PathToMaster" Inherits="System.Web.Mvc.ViewPage<ModelData>" %>
and I have a form with HTML markup for a set of checkboxes:
<label for="MyCheckbox">Your choice</label>
<input type="checkbox" id="Option1" class="checkbox" name="MyCheckbox" value="Option one" />
<label for="Option1">Option one</label><br />
<input type="checkbox" id="Option2" class="checkbox" name="MyCheckbox" value="Option two" />
<label for="Option2">Option two</label><br />
and I have a controller-action pair
class MyController : Controller {
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult RequestStuff( ModelData data )
{
}
}
and that action is invoked when the form is submitted.
How do I map the checkboxes onto members of ModelData (and what members I have to add to ModelData) so that when the form is submitted data stores information on which checkboxes are checked?
OK, this one will be for MVC3, but - save for syntax changes - should work in MVC2 too. The approach is essentially the same.
First of all, you should prepare an appropriate (view)model
public class MyViewModel
{
[DisplayName("Option 1")]
public bool Option1 { get; set; }
[DisplayName("Option 2")]
public bool Option2 { get; set; }
}
Then you pass this model to the view you're showing (controller):
public ActionResult EditMyForm()
{
var viewModel = new MyViewModel()
return View(viewModel);
}
with the form:
#model MyViewModel
#using( Html.BeginForm())
{
#Html.Label("Your choice")
#Html.LabelFor(model => model.Option1) // here the 'LabelFor' will show you the name you set with DisplayName attribute
#Html.CheckBoxFor(model => model.Option1)
#Html.LabelFor(model => model.Option2)
#Html.CheckBoxFor(model => model.Option2)
<p>
<input type="submit" value="Submit"/>
</p>
}
Now here the HTML helpers (all the CheckBoxFor, LabelFor, EditorFor etc) allow to bind the data to the model properties.
Now mind you, an EditorFor when the property is of type bool will give you the check-box in the view, too. :)
And then, when you submit to the controller, it will auto-bind the values:
[HttpPost]
public ActionResult EditMyForm(MyViewModel viewModel)
{
//And here the view model's items will be set to true/false, depending what you checked.
}
First you define SelectList for Options. This will be used just to render checkboxes
public IList<SelectListItem> OptionsSelectList { get; set; }
Than, you define model that will hold value of single chosen option after post
public class ChooseOptionViewModel
{
public int OptionIdentifier { get; set; } //name or id
public bool HasBeenChosen { get; set; } //this is mapped to checkbox
}
Then IList of those options in ModelData
public IList<ChooseOptionViewModel> Options { get; set; }
And finally, the view
#for (int i = 0; i < Model.OptionsSelectList.Count(); i++)
{
<tr>
<td class="hidden">
#Html.Hidden("Options[" + i + "].OptionIdentifier", Model.OptionsSelectList[i].Value)
</td>
<td>
#Model.OptionsSelectList[i].Text
</td>
<td>
#Html.CheckBox("Options[" + i + "].HasBeenChosen", Model.Options != null && Model.Options.Any(x => x.OptionIdentifier.ToString().Equals(Model.OptionsSelectList[i].Value) && x.HasBeenChosen))
</td>
</tr>
}
After post, you just inspect Options.Where(x => x.HasBeenChosen)
This is full-functional, and it will allow you to redisplay view when validation errors occur, etc. This seems a bit complicated, but I haven't come up with any better solution than this.

Categories

Resources