Validation ASP.NET MVC Child Elements - asp.net-mvc

I'm currently working on an ASP.NET MVC 3 application. I'm building a screen with out-of-the-box validation. Allow me to clarify the situation using the following screenshot.
Above you see a collection of TemplateItems. The second column 'Uitvoerder' is required. This works fine in most cases.
The problem however, is that it's not a regular list of items, but that it's structured to represent the hierarchy shown in the UI. Hence the second item is a child of the first, and thus contained in the first TemplateItem object you see.
Validation does not fire for the contained items.
You could argue that the front end model should be flattened and made less complex, but I'd like to avoid that. Is there any way I can have the validation trigger for the child elements as well?
The model looks like this:
public class WorkflowTemplateItemModel
: IValidatableObject
{
public WorkflowTemplateItemModel[] ChildWorkflowTemplateItems { get; set; }
public long? ExecutionParticipantId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ExecutionParticipantId == null)
{
yield return new ValidationResult("Contact needs to be specified",new[] {"ExecutionParticipantId"});
}
}
}
The relevant Razor part:
<td>
#Html.DropDownListFor(model => model.ExecutionParticipantId,
Model.AvailableUsers.Select(user => new SelectListItem
{
Text = user.UserName,
Value = user.Id.ToString(),
Selected = (Model.ExecutionParticipantId == user.Id)
}),
string.Empty
)
</td>
and the razor which builds the tree view:
#for (int i = 0; i < Model.ChildWorkflowTemplateItems.Length; i++)
{
#Html.EditorFor(model => model.ChildWorkflowTemplateItems[i], new { Depth = Depth + 1, ParentId = Model.WorkflowItemId, RootModel = GetViewData<CreateWorkflowModel> ("RootModel") })
}

You can do that but you will need to create a custom validation including the client-side validation code.
The process is similar to this one: http://haacked.com/archive/2009/11/18/aspnetmvc2-custom-validation.aspx

Related

ASP.Net Filter Dropdown list items from multiple tables

Summary:
I'm trying to use two DropDownList controls to filter the data that is currently being sorted and displayed in a view.
What we are going to learn
Creating the ViewController for One to Many and Many-to-Many relationships that could Filter the data using DropDownList
Possible Causes
If my DropdownList code is not terrible wrong, The ViewModel I'm using to display the data has no proper support for the DropDownList items.
In other words, the RazorView and my ViewModels are not compatible for what I'm trying to achieve. If I try to change my ViewModel or RazorView, I get an eldless loop of errors for my existing code.
OR The Linq Query needs an expert attention
Here is FilterViewModel.cs
public IEnumerable <App> Apps { get; set; }
public IEnumerable <Language> Languages { get; set; }
public IEnumerable <Platform> Platforms { get; set; }
public IEnumerable <AgeGroup> AgeGroups { get; set; }
public IEnumerable <ProductCode> ProductCodes { get; set; }
Here is AppsController.cs
public ActionResult FilterApps(App app)
{
var apps = _context.Apps.ToList();
var languages = _context.Languages.ToList();
var productCodes = _context.ProductCodes.ToList();
var platforms = _context.Platforms.ToList();
var ageGroups = _context.AgeGroups.ToList();
var viewModel = new FilterViewModel
{
AgeGroups = ageGroups,
Languages = languages,
Platforms = platforms,
ProductCodes = productCodes,
Apps = apps
.Where(a => a.LanguageId == app.LanguageId && a.PlatformId == app.PlatformId)
// I also tried all possible combinations :(a.Lanage.Id etc)
};
return View("FilterApps", viewModel);
}
Here is the FilterApps.cshtml
#model Marketing.ViewModels.FilterViewModel
<h2>FilterApps</h2>
#using (Html.BeginForm("FilterApps", "Apps", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="form-group">
#Html.DropDownListFor( m => m.Languages,
new SelectList(Model.Languages, "Id", "Name"),"Select Language",
new { #class = "form-control", #id = "dropDown" })
#Html.DropDownListFor(m => m.Platforms,
new SelectList(Model.Platforms, "Id", "Name"), "Select Platform",
new { #onchange = "this.form.submit();",
#class = "form-control", #id = "dropDown" })
</div>
}
//The existing code below is working fine so far.
#foreach (var group in Model.AgeGroups)
{
<h4>#group.Name</h4>
#foreach (var app in Model.Apps.OrderBy(a => a.AppOrder))
{
if (app.AgeGroupId == group.Id)
{
#app.ProductCode.Name
#app.Name
#app.Platform.Name
}
}
}
Probably unnecessary but I hope the additional information will help.
Additional Information
The App.cs is referencing all other tables e.g.
public Language Language { get; set; }
public int LanguageId { get; set; }
public Platform Platform { get; set; }
public int PlatformId { get; set; }
and so on...
What I have already tried
Several breakpoints and Logs to track the data, I also tried to use the following but it ruins my existing sorting and grouping.
public App App { get; set; } //Instead of the IEnumerable<App>
There are multiple issues with your code.
First you cannot bind a <select> element to a collection of complex objects. A <select> posts back the value of its selected option (which will be an int assuming the Id property of Language is int).
Next the view in the model is FilterViewModel (and your generating form controls with name attributes based on those properties), but your posting back to a different model (App) which does not contain those properties so nothing would bind anyway.
Your adding a null label option ("Select Language") and if that were selected, it would post a null value which would cause your query to fail.
There are also some bad practices which I have noted below.
Your view model should be
public class AppsFilterVM
{
public int? Language { get; set; }
public int? Platform { get; set; }
public IEnumerable<SelectListItem> LanguageOptions { get; set; }
public IEnumerable<SelectListItem> PlatformOptions { get; set; }
...
public IEnumerable <App> Apps { get; set; }
}
Its not clear what AgeGroups and ProductCodes are for so I have omitted them in the code above, and from your comments, I have assumed that the user can filter by either Language or Platform or both
The controller code would be
public ActionResult FilterApps(AppsFilterVM model)
{
var apps = _context.Apps;
if (model.Language.HasValue)
{
apps = apps.Where(x => x.LanguageId == model.Language.Value);
}
if (model.Platform.HasValue)
{
apps = apps.Where(x => x.PlatformId == model.Platform.Value);
}
model.Apps = apps;
ConfigureViewModel(model);
return View(model);
}
private void ConfigureViewModel(AppsFilterVM model)
{
// populate the selectlists
var languages = _context.Languages;
var platforms = _context.Platforms
model.LanguageOptions = new SelectList(languages, "Id", "Name");
model.PlatformOptions = new SelectList(platforms , "Id", "Name");
}
Then in the view (note its making a GET, not a POST)
#model.AppsFilterVM
....
#using (Html.BeginForm("FilterApps", "Apps", FormMethod.Get))
{
#Html.LabelFor(m => m.Language)
#Html.DropdownListFor(m => m.Language, Model.LanguageOptions, "No filter")
#Html.ValidationMessageFor(m => m.Language)
#Html.LabelFor(m => m.Platform)
#Html.DropdownListFor(m => m.Platform, Model.PlatformOptions, "No filter")
#Html.ValidationMessageFor(m => m.Platform)
<input type="submit" value="Filter" />
}
#foreach (var group in Model.AgeGroups)
{
....
There a a few other thing you should not be doing. Your giving both <select> elements the same id attribute which is invalid html (the DropDownListFor() method already generates a unique id based on the property name).
You should not submit a form based on the change event of a <select> Not only is it unexpected behavior, if a user uses the keyboard to navigate through options (e.g. using the arrow keys, or typing a character to go to the first option starting with that letter, then the form will be immediately submitted. In addition, the user might select an option from the 2nd dropdownlist first, which would immediately post before they have a chance to select the option in the first one. Allow the user to make their selections, check them, and then submit the form when they choose to.
Your view should not contain linq queries, and your grouping and ordering should be done in the controller before you pass the model to the view. Your Apps property should in fact be a view model containing a property for the group name, and a collection property for the Apps, (similar to the view models in your previous question) so that the view is simply
#foreach(var group in Model.AgeGroups)
{
#group.Name
foreach (var app in group.Apps)
{
#app.ProductCode
#app.Name
#app.Platform
}
}
You should also consider using ajax to submit your form, which would call separate server method that returns a partial view of just the Apps, and update the DOM in the success callback, which would improve performance. For an example, refer Rendering partial view on button click in ASP.NET MVC.

#model dynamic binding with arrays

I have a data class that I want to show via a View, allow the User to change it, and bring it back in via data binding. I want to do this via #model dynamic. Is this possible?
This is my model
public class MyData
{
public int A { get; set; }
public string B { get; set; }
}
Getting sent to a view like so
public ActionResult Index()
{
MyData[] myDataList = new MyData[2];
myDataList[0] = new MyData() { A = 2, B = "Fred" };
myDataList[1] = new MyData() { A = 3, B = "Sue" };
dynamic charts = (from ch in myDataList
select new
{
ch.A,
ch.B
}).AsEnumerable().Select(c => c.ToExpando());
return View(charts);
}
With the view like so (this is wrong)
#model IEnumerable<dynamic>
#foreach (dynamic item in Model)
{
<text>
#Html.TextBox("item.B")
</text>
}
I'm floundering a bit here as I don't know if this is even possible and I can't find many complex examples of #model dynamic
To be clear, this example is an academic one, what I want here is to understand what's possible with dynamic models in asp mvc
Any advice?
Thanks
Try this in your view:
#for(int i = 0; i < Model.Lenght; i++)
{
#Html.TextBoxFor(model => model[i].A)
#Html.TextBoxFor(model => model[i].B)
}
While you can get it to work, I wouldn't recommend it.
The simple fix IMHO is to use the ExpandoObject as model directly. There's no point in converting it to dynamic at all.

ASP.NET MVC 4 generating a treeview with recursive partial view

I have a partial view in an MVC 4 project, which is strongly typed. It takes an IEnumerable collection of a table of a database. In that table there are IDs, Names, and ParentIDs for storing hierarchical connection between records. The view that calls the partial view is also strongly typed, it takes the whole database as the model, and passes the Categories table to the partial view, as an enumerable collection:
#Html.Partial("_TreeCategories", #Model.Categories.ToList())
And in the partial view, I want to take the root nodes first, so I can extend the whole tree in a recursive way. In the database table, all records are considered as root nodes with a ParentID == null.
So generally, my way to do this would look like:
#model IEnumerable<TreeCollections.OpenAccess.Category>
#if (Model.ToList().Count >= 0)
{
#if (Model.ToList()[0].Parent_id == null)
{
<text><ul id="navigation"></text>
}
#foreach (var node in #Model)
{
<li>#node.Name
#foreach (var subNode in #Model.Where(s => s.Parent_id == node.Id))
{
#Html.Partial("_TreeCategories", subNode)
}
</li>
}
#if (Model.ToList()[0].Parent_id == null)
{
</ul>
}
}
So I check if the first element's ParentID of the Model is null, and if it is, then it should create a < ul> tag with the id "navigation", so the jquery plugin can recognise that it is meant to be a treeview control. Then it creates a list tag with a recursive call within. The recursively called partial view takes the children of the node as the model. And lastly if we arrived to the end of the partial view's rendering, and we are at the "root level", it should write a closing < ul> tag
There are some problems, however. First, at the end, that closing unordered list tag is wrong, VS can't find the matching start tag for that. Second, I don't know why, but at the top, I can put the starter < ul> tag in between tags, and I can't do it at the closing tag below. But I'm not sure about these < ul > tags either, I feel those are wrong too.
Please, help me, I'm stuck with this for days now.
man, you got some wonk going on here. i feel your pain on getting stuck.
see if this floats your boat.
you need a seed value to keep track of what you are looking for in the listing when you do recursion on the same list. it's better to do a parent children mapping in the class, but meh this was fun to do given your structure and should do the trick.
Models
namespace trash.Models
{
public class Category
{
public int ID { get; set; }
public int? Parent_ID { get; set; }
public string Name {get; set;}
}
public class SeededCategories
{
public int? Seed { get; set; }
public IList<Category> Categories { get; set; }
}
}
Controller (NOTE: you start the recursion chain by setting the Seed property to null which will pick up all the null parents)
namespace trash.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
IList<trash.Models.Category> categories = new List<trash.Models.Category>();
categories.Add(new trash.Models.Category { ID = 1, Parent_ID = null, Name = "Top1" });
categories.Add(new trash.Models.Category { ID = 2, Parent_ID = null, Name = "Top2" });
categories.Add(new trash.Models.Category { ID = 3, Parent_ID = 1, Name = "Top1Ring1" });
categories.Add(new trash.Models.Category { ID = 4, Parent_ID = 1, Name = "Top1Ring2" });
trash.Models.SeededCategories model = new Models.SeededCategories { Seed = null, Categories = categories };
return View(model);
}
}
}
Views
Index
#model trash.Models.SeededCategories
Here's a list
#Html.Partial("_TreeCategories", Model)
Partial (your _TreeCategories. NOTE: set the Seed to the current node ID and volia recursion)
#model trash.Models.SeededCategories
#if (Model.Categories.Where(s => s.Parent_ID == Model.Seed).Any())
{
<ul>
#foreach (var node in Model.Categories)
{
if (node.Parent_ID == Model.Seed)
{
trash.Models.SeededCategories inner = new trash.Models.SeededCategories { Seed = node.ID, Categories = Model.Categories };
<li>#node.Name
#Html.Partial("_TreeCategories", inner)
</li>
}
}
</ul>
}
You can try Shield UI's recursive TreeView for ASP.NET MVC.
It allows you to specify all the TreeView items using a RecursiveDataSource object, which can be setup to retrieve the data for a tree item from a remote endpoint or a local source "lazily", whenever the item is being expanded.
The RecursiveDataSource is a wrapper around a JavaScript DS widget, which introduces the need for some JS code, as well as updating your server code that will provide the data (either implementing a web service, or place the data in a JS variable in your view).

Rebinding MVC Models with multiple array selections

I have read somewhat on the post-redirect-get design pattern and I'm not sure if it works for my purpose as what I have is an MVC site which is design to look like an application, I have multiple dropdowns on the page which all bind to an integer array as below in my controller:
[HttpPost]
public ViewResult ResponseForm(PartyInvites.Models.GuestResponse response, int[] SelectedCustomer)
{
return View(response); // works but resets all my selected dropdowns
// return View(); // gives an error that it can't rebind items in view
}
My View:
#foreach (Schedule sched in Model.Schedules)
{
#Html.DropDownList("MySelectedCustomer", new SelectList(sched.Customers, "Id", "FirstName"), "Select A Customer", new { #class = "SelectedCustomer" })
}
The GuestResponse:
public class GuestResponse
{
[Required(ErrorMessage = "You must enter your name")]
public string Name { get; set; }
public string SomeString = "someString";
public string Email { get; set; }
public string Phone { get; set; }
public bool? WillAttend { get; set; }
public int SelectedSchedule = 0;
public int SelectedCustomer = 0;
public List<Schedule> Schedules
{
get
{
return new List<Schedule>() { new Schedule() { ScheduleName = "party1", ScheduleId = 1 }, new Schedule() { ScheduleId = 2, ScheduleName = "party2" } };
}
set
{
Schedules = value;
}
}
}
The SelectCustomer property is a property on the GuestResponse class. All the dropdowns are bound and if I change a few they bind nicely to the int[] SelectedCustomer collection. However I want to return my View back (so it does nothing essentially) but this resets all the dropdowns to their original state as the response was never fully bound because there was multiple dropdowns and MVC couldn't model bind to it. What it the best way of doing this so it maintains state so to speak?
The correct way to handle this is to use a view model instead of passing your domain models to the view.
But if you don't want to follow good practices you could generate your dropdowns like this as a workaround:
for (int i = 0; i < Model.Schedules.Count; i++)
{
#Html.DropDownList(
"MySelectedCustomer[" + i + "]",
new SelectList(
Model.Schedules[i].Customers,
"Id",
"FirstName",
Request["MySelectedCustomer[" + i + "]"]
),
"Select A Customer",
new { #class = "SelectedCustomer" }
)
}
The correct way is to have a property of type int[] SelectedCustomers on your view model and use the strongly typed version of the DropDownListFor helper:
for (int i = 0; i < Model.Schedules.Count; i++)
{
#Html.DropDownListFor(
x => x.SelectedCustomers,
Model.Schedules[i].AvailableCustomers,
"Select A Customer",
new { #class = "SelectedCustomer" }
)
}
and your POST controller action will obviously take the view model you defined as parameter:
[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
// The model.SelectedCustomers collection will contain the ids of the selected
// customers in the dropdowns
return View(model);
}
And since you mentioned the Redirect-After-Post design pattern, this is indeed the correct pattern to be used. In case of success you should redirect to a GET action:
[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
if (!ModelState.IsValid)
{
// the model is invalid => redisplay the view so that the user can fix
// the errors
return View(model);
}
// at this stage the model is valid => you could update your database with the selected
// values and redirect to some other controller action which in turn will fetch the values
// from the database and correctly rebind the model
GuestResponse domainModel = Mapper.Map<GuestResponseViewModel, GuestResponse>(model);
repository.Update(domainModel);
return RedirectToAction("Index");
}
Note: I'm first addressing why it's not binding anything, but that's not addressing the array issue, which I will get to afterwards. Where most people go wrong with MVC is that they do not take advantage of the built-in features of MVC to deal with these situations. They insist on doing foreach's and manually rendering things, but do not take into account the collection status.
The reason why the values are reset is because you are using Html.DropDownList() rather than Html.DropDownListFor(), and you are renaming the posted property name to a different name than your model property name.
You could simply change it to this:
#Html.DropDownList("SelectedCustomer", // note the removal of "My"
new SelectList(sched.Customers, "Id", "FirstName"),
"Select A Customer", new { #class = "SelectedCustomer" })
However, you would not have had this issue, and saved yourself a huge headache if you had just used the strongly typed version.
#Html.DropDownListFor(x => x.SelectedCustomer,
new SelectList(sched.Customers, "Id", "FirstName"),
"Select A Customer", new { #class = "SelectedCustomer" })
As for the Array, you should use an EditorTemplate for Schedules, and in that EditorTemplate you simply create your html as if it were a single item. That's the great thing about Editor/DisplayTemplates is that they automatically deal with collections.
Create a folder in your Views/Controller folder called EditorTemplates. In that folder, create an empty file called Schedule.cshtml (assuming Schedules is a List or array of Schedule). In that, you have code to render a single schedule.
EDIT:
Darin brings up a good point. I would make a small change to the model and add a Selected property to both Schedule and GuestResponse, then you can use Linq to return the selected schedule and it would simplify things.
EDIT2:
You some conflicts between the problem you've described and the code you've shown. I suggest you figure out exactly what you're trying to do, since your code does not really reflect a viable model for this.

MVC Dynamic View Data and Dynamic Views

Traditionally, I have built MVC applications using view models with Data Annotations attributes, and I dynamically render the views using editor templates. Everything works great, and it really cuts down on the time it takes me to build new views. My requirements have recently changed. Now, I can't define the view model at design time. The properties that will be rendered on the view are decided at run time based on business rules. Also, the validation rules for those properties may be decided at run time as well. (A field that is not required in my domain model, may be required in my view based on business rules). Also, the set of properties that will be rendered is not known until run time - User A may edit 6 properties from the model, while user B may edit 9 properties.
I am wondering if it is possible to create a model metadata provider that will supply my own metadata from business rules for an untyped view model like a collection of property names and values. Has anyone solved this problem?
I solved a similar problem by creating a more complex model, and using a custom editor template to make the model be rendered to look like a typical editor, but using the dynamic field information:
public class SingleRowFieldAnswerForm
{
/// <summary>
/// The fields answers to display.
/// This is a collection because we ask the MVC to bind parameters to it,
/// and it could cause issues if the underlying objects were being recreated
/// each time it got iterated over.
/// </summary>
public ICollection<IFieldAnswerModel> FieldAnswers { get; set; }
}
public interface IFieldAnswerModel
{
int FieldId { get; set; }
string FieldTitle { get; set; }
bool DisplayAsInput { get; }
bool IsRequired { get; }
bool HideSurroundingHtml { get; }
}
// sample implementation of IFieldAnswerModel
public class TextAreaFieldAnswer : FieldAnswerModelBase<TextAreaDisplayerOptions>
{
public string Answer { get; set; }
}
EditorTemplates/SingleRowFieldAnswerForm.cshtml:
#helper DisplayerOrEditor(IFieldAnswerModel answer)
{
var templateName = "FieldAnswers/" + answer.GetType().Name;
var htmlFieldName = string.Format("Answers[{0}]", answer.FieldId);
if (answer.DisplayAsInput)
{
#Html.EditorFor(m => answer, templateName, htmlFieldName)
// This will display validation messages that apply to the entire answer.
// This typically means that the input got past client-side validation and
// was caught on the server instead.
// Each answer's view must also produce a validation message for
// its individual properties if you want client-side validation to be
// enabled.
#Html.ValidationMessage(htmlFieldName)
}
else
{
#Html.DisplayFor(m => answer, templateName, htmlFieldName)
}
}
<div class="form-section">
<table class="form-table">
<tbody>
#{
foreach (var answer in Model.FieldAnswers)
{
if (answer.HideSurroundingHtml)
{
#DisplayerOrEditor(answer)
}
else
{
var labelClass = answer.IsRequired ? "form-label required" : "form-label";
<tr>
<td class="#labelClass">
#answer.FieldTitle:
</td>
<td class="form-field">
<div>
#DisplayerOrEditor(answer)
</div>
</td>
</tr>
}
}
}
</tbody>
</table>
</div>
So I populate my SingleRowFieldAnswerForm with a series of answer models. Each answer model type has its own editor template, allowing me to customize how different types of dynamic "properties" should be displayed. For example:
// EditorTemplates/FieldAnswers/TextAreaFieldAnswer.cshtml
#model TextAreaFieldAnswer
#{
var htmlAttributes = Html.GetUnobtrusiveValidationAttributes("Answer", ViewData.ModelMetadata);
// add custom classes that you want to apply to your inputs.
htmlAttributes.Add("class", "multi-line input-field");
}
#Html.TextAreaFor(m => m.Answer, Model.Options.Rows, 0, htmlAttributes)
#Html.ValidationMessage("Answer")
The next tricky part is that when you send this information to the server, it doesn't inherently know which type of IFieldAnswerModel to construct, so you can't just bind the SingleRowAnswerForm in your arguments list. Instead, you have to do something like this:
public ActionResult SaveForm(int formId)
{
SingleRowAnswerForm form = GetForm(formId);
foreach (var fieldAnswerModel in form.FieldAnswers.Where(a => a.DisplayAsInput))
{
// Updating this as a dynamic makes sure all the properties are bound regardless
// of the runtime type (since UpdateModel relies on the generic type normally).
this.TryUpdateModel((dynamic) fieldAnswerModel,
string.Format("Answers[{1}]", fieldAnswerModel.FieldId));
}
...
Since you provided MVC with each dynamic "property" value to bind to, it can bind each of the properties on each answer type without any difficulty.
Obviously I've omitted a lot of details, like how to produce the answer models in the first place, but hopefully this puts you on the right track.
You can use The ViewData Property in your ViewModel, View and Controller, it is dynamic, so it can be resolved at runtime.

Resources