MVC 4 - sorting with LINQ doesn't work with Ajax.BeginForm and my For loop - list

I writing some code with C# and MVC and I have button for sorting a list of data by asc and desc. The logic works in my controller, I am able to call the method that sorts the list and in the breakpoint I can see that it has been sorted.
But it's weird because when I loop through my list in the partial view it never works. I use a breakpoint in my view to make sure it's the same order of items which it is. But it's like the new values don't render to the screen.
TeamManagement.cshtml
#model Website.Models.modelTeamSelect
#{
ViewBag.Title = "Football App";
}
#section featured {
}
#using (Ajax.BeginForm("_PartialTeams",
new
{
model = this.Model
},
new AjaxOptions
{
HttpMethod = "POST",
UpdateTargetId = "divCreatedTeams",
InsertionMode = InsertionMode.Replace
}))
{
<div id="divTeams" style="float: left; padding: 10px;">
<h3>Create a new team:</h3>
#Html.LabelFor(m => m.team.TeamName)
#Html.TextBoxFor(m => m.team.TeamName)
<input type="submit" value="Add Team" name="btnSubmit" />
</div>
Html.RenderPartial("~/Views/Partials/_PartialTeams.cshtml");
}
_PartialTeams.cshtml
#model Website.Models.modelTeamSelect
<div id="divCreatedTeams" style="float: left; padding: 10px;">
<h3>Your created teams:</h3>
<input type="submit" value="Asc" name="btnSubmit" />
<input type="submit" value="Desc" name="btnSubmit" />
<br />
#if (Model.teams.Count > 0)
{
for (int i = 0; i < Model.teams.Count; i++)
{
#Html.EditorFor(m => m.teams[i].TeamName)
<input type="button" value="Update team name" name="btnSubmit"/>
<input type="button" value="Remove team" name="btnSubmit"/>
<br />
}
}
</div>
Sorting logic in my controller
[HttpPost]
public PartialViewResult _PartialTeams(string BtnSubmit, modelTeamSelect modelTeamSelect)
{
switch (BtnSubmit)
{
case "Add Team":
modelTeamSelect.teams.Add(modelTeamSelect.team);
break;
case "Asc":
FootballRepository = new Repository.FootballRepository();
modelTeamSelect.teams = FootballRepository.Sort(modelTeamSelect, BtnSubmit);
break;
case "Desc":
FootballRepository = new Repository.FootballRepository();
modelTeamSelect.teams = FootballRepository.Sort(modelTeamSelect, BtnSubmit);
break;
}
return PartialView("~/Views/Partials/_PartialTeams.cshtml", modelTeamSelect);
}
public List<Models.modelTeam> Sort(Models.modelTeamSelect modelTeamSelect, string sort)
{
switch (sort)
{
case "Asc":
modelTeamSelect.teams = modelTeamSelect.teams.OrderBy(t => t.TeamName).ToList();
break;
case "Desc":
modelTeamSelect.teams = modelTeamSelect.teams.OrderByDescending(t => t.TeamName).ToList();
break;
}
return modelTeamSelect.teams;
}
My main model with team collection
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Website.Models
{
public class modelTeamSelect
{
public modelTeamSelect()
{
teams = new List<modelTeam>();
team = new modelTeam();
}
public List<modelTeam> teams { get; set; }
public modelTeam team { get; set; }
}
}
My method Sort does it's job but in the view it never displays correctly. e.g. always wrong order.
Anyone have any ideas because I am stuck.
Screenshots
In the screenshots I click sort by Asc and you can see it says Newcastle as the first item in the list. But when the page renders it will say West Ham first even though it is iterating using the for loop.

All the Html helpers are preferring to use the ModelState values over the actual model values.
So even you have sorted in place your modelTeamSelect.teams in your action in the view #Html.EditorFor(m => m.teams[i].TeamName) call will use the original (before sorting) values form the ModelState.
The solution: if you are updating your action parameters in-place then just clear the ModelState before returning the View/PartialView:
[HttpPost]
public PartialViewResult _PartialTeams(string BtnSubmit,
modelTeamSelect modelTeamSelect)
{
// ... Do the sorting, etc.
ModelState.Clear();
return PartialView("~/Views/Partials/_PartialTeams.cshtml", modelTeamSelect);
}
You can read more about why the helpers are working like this in this article: ASP.NET MVC Postbacks and HtmlHelper Controls ignoring Model Changes

Related

How to replace `#computed` with setter returning new value with new native setters?

Problem
I've often used this kind of computed properties where the setter simply returns the new value :
#computed('args.myValue')
get myValue() {
return this.args.myValue;
}
set myValue(newValue) {
return newValue; // <==== this is no longer valid with native setter
}
This does few things :
Set initial value to args.myValue
Allow to change the value (typically through an <Input #value={{this.myValue}} />)
Restore the default value when args.myValue changes
The problem comes with native setters which can't return any value.
Notice I could probably find a "hackish" solution but I'd like to have code that follows new EmberJS conventions in order to avoid painfull later updates.
Things I tried
Manual caching
#tracked _myValue = null;
get myValue() {
return this._myValue || this.args.myValue;
}
set myValue(newValue) {
this._myValue = newValue;
}
This does not work because _myValue is always set after the first myValue=(newValue).
In order to make it work, there should be some kind of observer which resets it to null on args.myValue change.
Sadly, observers are no longer part of EmberJS with native classes.
{{unbound}} helper
<Input #value={{unbound this.myValue}} />
As expected, it does not work because it just doesn't update myValue.
{{unbound}} helper combined with event.target.value handling
<Input #value={{unbound this.myValue}} {{on "keyup" this.keyPressed}} />
get myValue() {
return this.args.myValue;
}
#action keyPressed(event) {
this.doStuffThatWillUpdateAtSomeTimeMyValue(event.target.value);
}
But the Input is still not updated when the args.myValue changes.
Initial code
Here is a more concrete use example :
Component
// app/components/my-component.js
export default class MyComponent extends Component {
#computed('args.projectName')
get projectName() {
return this.args.projectName;
}
set projectName(newValue) {
return newValue; // <==== this is no longer valid with native setter
}
#action
searchProjects() {
/* event key stuff omitted */
const query = this.projectName;
this.args.queryProjects(query);
}
}
{{! app/components/my-component.hbs }}
<Input #value={{this.projectName}} {{on "keyup" this.searchProjects}} />
Controller
// app/controllers/index.js
export default class IndexController extends Controller {
get entry() {
return this.model.entry;
}
get entryProjectName() {
return this.entry.get('project.name');
}
#tracked queriedProjects = null;
#action queryProjects(query) {
this.store.query('project', { filter: { query: query } })
.then((projects) => this.queriedProjects = projects);
}
#action setEntryProject(project) {
this.entry.project = project;
}
}
{{! app/templates/index.hbs }}
<MyComponent
#projectName={{this.entryProjectName}}
#searchProjects={{this.queryProjects}} />
When the queriedProjects are set in the controller, the component displays them.
When one of those search results is clicked, the controller updates the setEntryProject is called.
According to this Ember.js discussion :
Net, my own view here is that for exactly this reason, it’s often better to use a regular <input> instead of the <Input> component, and to wire up your own event listeners. That will make you responsible to set the item.quantity value in the action, but it also eliminates that last problem of having two different ways of setting the same value, and it also gives you a chance to do other things with the event handling.
I found a solution for this problem by using standard <input>, which seems to be the "right way" to solve it (I'll really appreciate any comment that tells me a better way) :
{{! app/components/my-component.hbs }}
<input value={{this.projectName}} {{on "keyup" this.searchProjects}} />
// app/components/my-component.js
#action
searchProjects(event) {
/* event key stuff omitted */
const query = event.target.value;
this.args.queryProjects(query);
}
If I needed to keep the input value as a property, I could have done this :
{{! app/components/my-component.hbs }}
<input value={{this.projectName}}
{{on "input" this.setProjectQuery}}
{{on "keyup" this.searchProjects}} />
// app/components/my-component.js
#action setProjectQuery(event) {
this._projectQuery = event.target.value;
}
#action
searchProjects( {
/* event key stuff omitted */
const query = this._projectQuery;
this.args.queryProjects(query);
}
EDIT
Notice the following solution has one downside : it does not provide a simple way to reset the input value to the this.projectName when it does not change, for example after a focusout.
In order to fix this, I've added some code :
{{! app/components/my-component.hbs }}
<input value={{or this.currentInputValue this.projectName}}
{{on "focusin" this.setCurrentInputValue}}
{{on "focusout" this.clearCurrentInputValue}}
{{on "input" this.setProjectQuery}}
{{on "keyup" this.searchProjects}} />
// app/components/my-component.js
// previous code omitted
#tracked currentInputValue = null;
#action setCurrentInputValue() {
this.currentInputValue = this.projectName;
}
#action clearCurrentInputValue() {
this.currentInputValue = null;
}
There is a quite generic and concise approach to this 2-source binding scenario with any interactive input element and beyond.
Considering your first attempt (»Manual Caching«):
we have a functional feedback loop through the getter and setter; no return value from the setter is required since it unconditionally triggers a bound getter (this._myValue doesn't need to be tracked)
a switch is needed to let a changing external preset value (this.args.myValue) inject into this loop
this is accomplished by a GUID hashmap based on the preset value that establishes a transient scope for the interactive input; thus, changing preset value injections and interative inputs overwrite each other:
// app/components/my-component.js
import Component from '#glimmer/component';
import { guidFor } from '#ember/object/internals';
export default class extends Component {
// external preset value by #stringArg
_myValue = new Map();
get myValue() {
let currentArg = this.args.stringArg || null;
let guid = guidFor(currentArg);
if (this._myValue.has(guid)) {
return this._myValue.get(guid)
}
else {
this._myValue.clear(); // (optional) avoid subsequent GUID reuse of primitive types (Strings)
return currentArg;
}
}
set myValue(value) {
this._myValue.set(guidFor(this.args.stringArg || null), value);
}
}
// app/components/my-component.hbs
<Input #value={{mut this.myValue}} />
https://ember-twiddle.com/a72fa70c472dfc54d03d040f0d849d17

NullReferenceException while passing list of object from view to controller

I am trying to save list of object from view to controller but i am getting NullReferenceException when list is more that 25. It works fine if list less than 25.
public async Task<IActionResult> ImportStudentExcel(IFormFile file)
{
var list = new List<StudentImport>();
//Here it contains logic for adding item to list from excel file
ViewBag.FileName = file.FileName;
return View(list.ToList());
}
I am getting all the item in my view
I am doing this to bind properties
//Containes Table for Showing List
<form id="saveForm" asp-action="SaveFromImport" asp-controller="StudentImport" method="POST">
<input type="hidden" name="filename" value="#ViewBag.FileName">
#for(int i=0; i<Model.Count; i++)
{
<input asp-for="#Model[#i].Fullname" type="hidden" value="#Model[#i].Fullname"/>
<input asp-for="#Model[#i].Gender" type="hidden" value="#Model[#i].Gender"/>
<input asp-for="#Model[#i].DOB" type="hidden" value="#Model[#i].DOB"/>
// Other 15 Properties Like Address, PhoneNumber, RegNo etc
}
<input type="submit" value="Save">
</form>
When I inspect this page all item are present
public async Task<IActionResult> SaveFromImport(List<StudentImport> students, string filename)
{
try
{
foreach (var t in students)
{
System.Console.WriteLine(t.Fullname);
//Save to DB
}
}
catch (Exception e)
{
System.Console.WriteLine(e.ToString());
}
return RedirectToAction("Index", "Student");
}
Am getting NullReference at foreach Statement. I dont know whats going on. It works as expected when list count is 13 but wont work when count is 25 or more, It also works when there is only one property in StudentImportModel and count is sttil 25.
The ExceptionMessage in my case was NullReferenceException but the actual error was InvalidDataException: Form value count limit 1024 exceeded.
However, I managed to solve this by adding this code in ConfigureServices method.
services.Configure<FormOptions>(options =>
{
options.ValueCountLimit = 6000;
});

How to display foreign key name in list table in Blazor Client Side

I am trying to display a subject name for a course where I saved the subject from a database driven dropdown list in a client-side Blazor app. The value returns as a Guid instead of the subject name. Has anyone accomplished this with Blazor? I couldn't find anything in the Blazor documentation or any tutorials that could solve the issue I'm having. This is in Blazor client-side and I am using Entity Framework Core
This is what my Course model looks like in the Shared project:
public class Course
{
public Guid CourseID { get; set; }
[Required]
public string CourseCode { get; set; }
[Required]
public string CourseName { get; set; }
public string CourseSubject { get; set; }
public string CourseCredits { get; set; }
}
This is what my Subject model looks like in the Shared project:
public class Subject
{
public Guid SubjectID { get; set; }
public string SubjectName { get; set; }
}
This is my CourseData Data Access Model in the Server project:
ApplicationDbContext db = new ApplicationDbContext ();
public IEnumerable<Course> GetAllCourses()
{
try
{
return db.Courses.ToList();
}
catch
{
throw;
}
}
public void AddCourse(Course course)
{
try
{
db.Courses.Add(course);
db.SaveChanges();
}
catch
{
throw;
}
}
This is my SubjectData Data Access Model in the Server project:
ApplicationDbContext db = new ApplicationDbContext ();
public IEnumerable<Subject> GetAllSubjects()
{
try
{
return db.Subjects.ToList();
}
catch
{
throw;
}
}
public void AddSubject(Subject subject)
{
try
{
db.Subjects.Add(subject);
db.SaveChanges();
}
catch
{
throw;
}
}
This is my Course Controller in the Server project:
CourseData objcourse = new CourseData();
[HttpGet]
[Route("api/Courses/Courses")]
public IEnumerable<Course> Index()
{
return objcourse.GetAllCourses();
}
[HttpPost]
[Route("api/Courses/Create")]
public void Create([FromBody] Course course)
{
if (ModelState.IsValid)
objcourse.AddCourse(course);
}
This is how I save the value in my Course creation page in my Client project:
#page "/Courses/Create"
#inject HttpClient Http
#inject Microsoft.AspNetCore.Components.NavigationManager NavigationManager
<h1>Add Course</h1>
<hr />
<div class="row">
<div class="col-md-4">
<div>
<div class="form-group">
<label for="CourseCode" class="control-label">Course Code</label>
<input for="CourseCode" class="form-control" #bind="#course.CourseCode" />
</div>
<div class="form-group">
<label for="CourseName" class="control-label">Course Name</label>
<input for="CourseName" class="form-control" #bind="#course.CourseName" />
</div>
<div class="form-group">
<label for="CourseSubject" class="control-label">Subject</label>
<select class="form-control" #bind="#course.CourseSubject">
<option></option>
#foreach (var subject in subjectList)
{
<option value="#subject.SubjectID">#subject.SubjectName</option>
}
</select>
</div>
<div class="form-group">
<label for="CourseCredits" class="control-label">Course Credits</label>
<input for="CourseCredits" class="form-control" #bind="#course.CourseCredits" />
</div>
<div class="form-group">
<button type="submit" class="btn btn-default" #onclick="#CreateCourse">Save</button>
<button class="btn" #onclick="#cancel">Cancel</button>
</div>
</div>
</div>
</div>
#functions {
List<Subject> subjectList = new List<Subject>();
Course course = new Course();
protected override async Task OnInitializedAsync()
{
subjectList = await Http.GetJsonAsync<List<Subject>>("api/Subjects/Subjects");
}
protected async Task CreateCourse()
{
await Http.SendJsonAsync(HttpMethod.Post, "/api/Courses/Create", course);
NavigationManager.NavigateTo("/Courses/Courses");
}
void cancel()
{
NavigationManager.NavigateTo("/Courses/Courses");
}
}
And finally this is my Courses list page in my Client project where it returns the Guid for the subject name, for which I would like to show the subject name instead of it's Guid:
#page "/Courses/Courses"
#inject HttpClient Http
<h1>Courses</h1>
<p>
Create New
</p>
#if (courseList == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class='table'>
<thead>
<tr>
<th>Course Code</th>
<th>Course Name</th>
<th>Subject</th>
<th>Credits</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
#foreach (var course in courseList)
{
<tr>
<td>#course.CourseCode</td>
<td>#course.CourseName</td>
<td>#course.CourseSubject</td>
<td>#course.CourseCredits</td>
<td>
<a href='/Courses/Edit/#course.CourseID'>Edit</a> |
<a href='/Courses/Delete/#course.CourseID'>Delete</a>
</td>
</tr>
}
</tbody>
</table>
}
#functions {
Course[] courseList;
protected override async Task OnInitializedAsync()
{
courseList = await Http.GetJsonAsync<Course[]>
("/api/Courses/Courses");
}
}
Database Context as requested:
public class ApplicationDbContext : DbContext
{
public virtual DbSet<Course> Courses { get; set; }
public virtual DbSet<Subject> Subjects { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(#"REMOVED-FOR-SECURITY");
}
}
}
Help would be much appreciated.
Other answers are good but let me give you something else to think about...
When writing Blazor apps try to think in terms of components more often than of code.
For example, let's say that the Subjects are static data that doesn't change a lot. So if every time you show a list, or want to build a dropdown for them, you're potentially going to have to make a database call or a SQL JOIN? So my first thought would be to create a lookup cache for GUID-to-Subject-name. And since this is Blazor if it's cached on the client you no longer need to make a database or API call - it's loaded once.
Then, since we're in Blazor, I'd create a <SubjectName> component, e.g.
Subject: <SubjectName Id="#Model.SubjectId" />
The component uses the cache to get the value, it can handle a null value, it can handle an invalid subject ID. You've encapsulated a lot of behaviour and error handling in a single place, and can re-use that every time you need to map an ID to a subject name.
Similarly I'd create a <SubjectDropdown> component to show a list of subjects, again using the cache.
You set CourseSubject with the SubjectID with is a Guid, so you get a Guid.
If you want to display the subject name either, request the subject by its Guid, or return the Subject in your course:
public class Course
{
public Guid CourseID { get; set; }
[Required]
public string CourseCode { get; set; }
[Required]
public string CourseName { get; set; }
public string CourseSubject { get; set; }
public string CourseCredits { get; set; }
public virtual Subject Subject { get; set; }
}
public IEnumerable<Course> GetAllCourses()
{
try
{
return db.Courses
.Include(c => c.Subject)
.ToList();
}
catch
{
throw;
}
...
}
#foreach (var course in courseList)
{
<tr>
<td>#course.CourseCode</td>
<td>#course.CourseName</td>
<td>#course.Subject.SubjectName</td>
<td>#course.CourseCredits</td>
<td>
<a href='/Courses/Edit/#course.CourseID'>Edit</a> |
<a href='/Courses/Delete/#course.CourseID'>Delete</a>
</td>
</tr>
}

CheckBoxList multiple selections: how to model bind back and get all selections?

This code:
Html.CheckBoxList(ViewData.TemplateInfo.HtmlFieldPrefix, myList)
Produces this mark-up:
<ul><li><input name="Header.h_dist_cd" type="checkbox" value="BD" />
<span>BD - Dist BD Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="SS" />
<span>SS - Dist SS Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="DS" />
<span>DS - Dist DS Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="SW" />
<span>SW - Dist SW Name </span></li>
</ul>
You can check multiple selections. The return string parameter Header.h_dist_cd only contains the first value selected. What do I need to do to get the other checked values?
The post method parameter looks like this:
public ActionResult Edit(Header header)
I'm assuming that Html.CheckBoxList is your extension and that's markup that you generated.
Based on what you're showing, two things to check:
The model binder is going to look for an object named Header with string property h_dist_cd to bind to. Your action method looks like Header is the root view model and not a child object of your model.
I don't know how you are handling the case where the checkboxes are cleared. The normal trick is to render a hidden field with the same name.
Also a nit, but you want to use 'label for="..."' so they can click the text to check/uncheck and for accessibility.
I've found that using extensions for this problem is error prone. You might want to consider a child view model instead. It fits in better with the EditorFor template system of MVC2.
Here's an example from our system...
In the view model, embed a reusable child model...
[AtLeastOneRequired(ErrorMessage = "(required)")]
public MultiSelectModel Cofamilies { get; set; }
You can initialize it with a standard list of SelectListItem...
MyViewModel(...)
{
List<SelectListItem> initialSelections = ...from controller or domain layer...;
Cofamilies = new MultiSelectModel(initialSelections);
...
The MultiSelectModel child model. Note the setter override on Value...
public class MultiSelectModel : ICountable
{
public MultiSelectModel(IEnumerable<SelectListItem> items)
{
Items = new List<SelectListItem>(items);
_value = new List<string>(Items.Count);
}
public int Count { get { return Items.Count(x => x.Selected); } }
public List<SelectListItem> Items { get; private set; }
private void _Select()
{
for (int i = 0; i < Items.Count; i++)
Items[i].Selected = Value[i] != "false";
}
public List<SelectListItem> SelectedItems
{
get { return Items.Where(x => x.Selected).ToList(); }
}
private void _SetSelectedValues(IEnumerable<string> values)
{
foreach (var item in Items)
{
var tmp = item;
item.Selected = values.Any(x => x == tmp.Value);
}
}
public List<string> SelectedValues
{
get { return SelectedItems.Select(x => x.Value).ToList(); }
set { _SetSelectedValues(value); }
}
public List<string> Value
{
get { return _value; }
set { _value = value; _Select(); }
}
private List<string> _value;
}
Now you can place your editor template in Views/Shared/MultiSelectModel.ascx...
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<WebUI.Cofamilies.Models.Shared.MultiSelectModel>" %>
<div class="set">
<%=Html.LabelFor(model => model)%>
<ul>
<% for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
string name = ViewData.ModelMetadata.PropertyName + ".Value[" + i + "]";
string id = ViewData.ModelMetadata.PropertyName + "_Value[" + i + "]";
string selected = item.Selected ? "checked=\"checked\"" : "";
%>
<li>
<input type="checkbox" name="<%= name %>" id="<%= id %>" <%= selected %> value="true" />
<label for="<%= id %>"><%= item.Text %></label>
<input type="hidden" name="<%= name %>" value="false" />
</li>
<% } %>
</ul>
<%= Html.ValidationMessageFor(model => model) %>
Two advantages to this approach:
You don't have to treat the list of items separate from the selection value. You can put attributes on the single property (e.g., AtLeastOneRequired is a custom attribute in our system)
you separate model and view (editor template). We have a horizontal and a vertical layout of checkboxes for example. You could also render "multiple selection" as two listboxes with back and forth buttons, multi-select list box, etc.
I think what you need is how gather selected values from CheckBoxList that user selected and here is my solution for that:
1- Download Jquery.json.js and add it to your view as reference:
2- I've added a ".cssMyClass" to all checkboxlist items so I grab the values by their css class:
<script type="text/javascript" >
$(document).ready(function () {
$("#btnSubmit").click(sendValues);
});
function populateValues()
{
var data = new Array();
$('.myCssClas').each(function () {
if ($(this).attr('checked')) {
var x = $(this).attr("value");
data.push(x);
}
});
return data;
}
function sendValues() {
var data = populateValues();
$.ajax({
type: 'POST',
url: '#Url.Content("~/Home/Save")',
data: $.json.encode(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function () { alert("1"); }
});
}
</script>
3- As you can see I've added all selected values to an Array and I've passed it to "Save" action of "Home" controller by ajax 4- in Controller you can receive the values by adding an array as argument:
[HttpPost]
public ActionResult Save(int[] val)
{
I've searched too much but apparently this is the only solution. Please let me know if you find a better solution for it.
when you have multiple items with the same name you will get their values separated with coma

MVC Templated Helper - DropDown

Using the templated helpers in MVC2.0 I ran into a dillema, how to get the items to fill a dropdownlist.
I am using a [UIHint(BadgesDropDown)] attribute, but how will i get the list items without violating the MVC Pattern, should the controller place them in the ViewData? Should the BadgesDropDown.ascx invoke a Helper to get them ?
Right now i am going for:
BadgesDropDown.ascx
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.DropDownList("", ViewData["Badges"] as IEnumerable<SelectListItem>)%>
Controller
ViewData["Badges"] = new SelectList(SiteRepository.GetBadges(), "RowKey", "BadgeName");
Is this the way to go ?
There's been a lot of discussion about this topic recently. Similar roadblocks are encountered with dates, date ranges and multi-select checkbox lists. Anywhere you might want to use a rich set of html controls. I've been experimenting with the concept of child ViewModels and I think the solution is cleaner than other approaches I've tried.
The basic concept is that you define a small view model that's closely coupled to a custom EditorTemplate.
In your example, we would start with a (child) ViewModel that's specific to a single select list:
public class SelectModel
{
#region SelectModel(string value, IEnumerable<SelectListItem> items)
public SelectModel(string value, IEnumerable<SelectListItem> items)
{
_value = value;
Items = new List<SelectListItem>(items);
_Select();
}
#endregion
// Properties
public List<SelectListItem> Items { get; private set; }
public string Value
{
get { return _value; }
set { _value = value; _Select();}
}
private string _value;
// Methods
private void _Select()
{
Items.ForEach(x => x.Selected = (Value != null && x.Value == Value));
}
}
In the view model that wants to use the dropdown you compose the select model (we're all using view models, right?):
public class EmailModel
{
// Constructors
public EmailModel()
{
Priority = new SelectModel("normal", _ToPrioritySelectItems());
}
// Properties
public SelectModel Priority { get; set; }
// Methods
private IEnumerable<SelectListItem> _ToPrioritySelectItems()
{
List<SelectListItem> result = new List<SelectListItem>();
result.Add(new SelectListItem() { Text = "High", Value = "high" });
...
}
Note this is a simple example with a fixed set of dropdown items. If they are coming from the domain layer, the controller passes them into the ViewModel.
Then add an editor template SelectModel.ascx in Shared/EditorTemplates
<%# Control Inherits="System.Web.Mvc.ViewUserControl<SelectModel>" %>
<div class="set">
<%= Html.LabelFor(model => model) %>
<select id="<%= ViewData.ModelMetadata.PropertyName %>_Value" name="<%=ViewData.ModelMetadata.PropertyName %>.Value">
<% foreach (var item in Model.Items) { %>
<%= Html.OptionFor(item) %>
<% } %>
</select>
</div>
Note: OptionFor is a custom extension that does the obvious
The trick here is that the id and name are set using the compound format that the default ModelBinder expects. In our example "Priority.Value". So the string based Value property that is defined as part of SelectModel is set directly. The setter takes care of updating the list of Items to set the default select option if we need to redisplay the form.
Where this "child view model" approach really shines is more complex "control snippets of markup". I now have child view models that follow a similar approach for MultiSelect lists, Start/End date ranges, and Date + time combinations.
As soon as you go down this path, the next obvious question becomes validation.
I ended up having all of my child ViewModel's implement a standard interface:
public interface IValidatable
{
bool HasValue { get; }
bool IsValid { get; }
}
Then, I have a custom ValidationAttribute:
public class IsValidAttribute : ValidationAttribute
{
// Constructors
public IsValidAttribute()
{
ErrorMessage = "(not valid)";
}
// Properties
public bool IsRequired { get; set; }
// Methods
private bool Is(object value)
{
return value != null && !"".Equals(value);
}
public override bool IsValid(object value)
{
if (!Is(value) && !IsRequired)
return true;
if (!(value is IValidatable))
throw new InvalidOperationException("IsValidAttribute requires underlying property to implement IValidatable");
IValidatable validatable = value as IValidatable;
return validatable.IsValid;
}
}
Now you can just put attributes on properties that are child ViewModel based like any scalar property:
[IsValid(ErrorMessage = "Please enter a valid start date/time")]
public DateAndTimeModel Start { get; set; }
In the MVC 2 a great new method... which if used relies on all the attribute data.
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<glossaryDB.EntityClasses.AssociationEntity>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Association: Edit
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="MainContent" runat="server">
<h3>Association: Edit</h3>
<% using (Html.BeginForm()) { %>
<fieldset style="padding: 1em; margin: 0; border: solid 1px #999;">
<%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
<%= Html.EditorForModel() %>
<input type="submit" value=" Submit " />
</fieldset>
<% } %>
<p><%= Html.ActionLink("Details", "Index") %></p>
</asp:Content>
For this to work there is 2 options. Either the UIHint has to provide the source of the data or the controller must. If the UIHint does then the data provided to thhe dropdown is fixed. The other option is the controller, which allows us to switch out the dropdown data with a different set of data as reqired.
There is some related examples I found:
Nerd Dinner
[1]: searcch for codeclimber.net.nz and how-to-create-a-dropdownlist-with-asp.net-mvc
[2]: bradwilson.typepad.com and templates-part-5-master-page-templates
I implemented the solution as the above example. One thing to be noted is that Helpers should only work with the data supplied to them, see View dependency
The best practice is to write Html
helpers unaware of controllers and
contexts. They should do their job
only based on what data is supplied by
the caller.
I agree on the above statement. It's just that a lot of work needs to be done when compared to the regular ASP.Net development.