I have found couple questions about my problem but none of them actually addresses the problem and gives alternative solutions to problem, thats why I am asking this again.
I am using strongly typed HTML Helpers and no ViewData and ViewBag for only page titles.
Here is my problem.
I have following viewmodel.
public class RegisterViewModel
{
public string Mail { get; set; }
public string Name { get; set; }
public ThreePartDatePickerViewModel ThreePartDateSelection { get; set; }
public RegisterViewModel()
{
ThreePartDateSelection = new ThreePartDatePickerViewModel();
}
}
Above viewmodel uses below viewmodel which basically holds data for 3 dropdownlist which are Day, Month and Year.
public class ThreePartDatePickerViewModel
{
public string Day { get; set; }
public string Year { get; set; }
public string Month { get; set; }
public IList<SelectListItem> Years { get; set; }
public IList<SelectListItem> Months { get; set; }
public IList<SelectListItem> Days { get; set; }
public ThreePartDatePickerViewModel()
{
var now = DateTime.Now;
Years = new List<SelectListItem>();
Months = new List<SelectListItem>();
Days = new List<SelectListItem>();
var empty = new SelectListItem { Text = "" };
Years.Add(empty);
Months.Add(empty);
Days.Add(empty);
foreach(var item in Enumerable.Range(0, 100).Select(x => new SelectListItem { Value = (now.Year - x).ToString(), Text = (now.Year - x).ToString() }))
Years.Add(item);
foreach(var item in Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToString(), Text = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x) }))
Months.Add(item);
foreach(var item in Enumerable.Range(1, 31).Select(x => new SelectListItem { Value = x.ToString(), Text = x.ToString() }))
Days.Add(item);
}
}
In the action method which returns RegisterViewModel to view, I am setting the Day,Month and Year properties of ThreePartDatePickerViewModel.
I have checked these values on runtime while views are generated and they are correct.
In my main view,
#model Bla.Bla.RegisterViewModel
<!-- Helpers for other properties -->
#Html.EditorFor(m => m.ThreePartDateSelection)
And my ThreePartDatePickerViewModel Editor Template is
#model Bla.Bla.ThreePartDatePickerViewModel
<div class="threePartDatePicker">
#Html.DropDownListFor(m => m.Day, Model.Days)
#Html.DropDownListFor(m => m.Month, Model.Months)
#Html.DropDownListFor(m => m.Year, Model.Years)
</div>
In my final rendered html, I have all the controls as expected. Dropdowns are rendered fine except the values I have set in action method are not selected.
If I switch from EditorTemplates to Partial Views, It starts working. Or If directly render dropdownlists inside my main view, instead of passing model to EditorFor, It again works.
It seems an old bug, not solved yet. it is reported here.
http://connect.microsoft.com/VisualStudio/feedback/details/654543/asp-net-mvc-possible-mvc-bug-when-working-with-editortemplates-and-drop-down-lists#details
I have to edit the editor template to set selected value as,
#{
//capture the selected list wherever it is. Here it passed in additionalViewData
SelectList tmpList, list = null;
ViewContext.ViewData.TryGetValue("list", out tmpList);
if (tmpList != null && tmpList.Count() > 0 && tmpList.SelectedValue == null)
{
list = new SelectList(tmpList, "Value", "Text", Model);
}
else
{
list = tmpList;
}
}
<div class="form-group">
#Html.DropDownListFor(m => m, list, attributes)
</div>
so I can work with select list in Editor Template as default behavior it should be.
I'm experiencing the same issue using a dropdownlist in an editor template. The selected value is not set even though I can see the correct value in the model. This is my workaround. It could be extended to work with other DropdownListFor methods.
public static class HtmlHelperExtensions
{
public static IHtmlString EditorDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel)
{
var dropDown = SelectExtensions.DropDownListFor(htmlHelper, expression, selectList, optionLabel);
var model = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model;
if (model == null)
{
return dropDown;
}
var dropDownWithSelect = dropDown.ToString().Replace("value=\"" + model.ToString() + "\"", "value=\"" + model.ToString() + "\" selected");
return new MvcHtmlString(dropDownWithSelect);
}
}
Related
I have a get page that renders the edit page that looks like this
public ActionResult EditItemInstance(int id)
{
ItemInstance i = db.ItemInstances.Find(id);
var item = (from it in db.Items.Where(x => x.deleted == false)
select new
{
itemID = it.ID,
itemName = it.ItemID + ": " + it.Name
}).OrderBy(x => x.itemName).ToList();
ViewBag.ItemID = new SelectList(item, "itemID", "itemName", i.ItemID);
return View(i);
}
And in my view page I have a dropdown list that looks like this
#Html.DropDownList("ItemID", null, "-- Select --", htmlAttributes: new { #class = "form-control chosen-select" })
I want the default value to be the value of the current item I am editing. For most of the items this works correctly. But when I edit some items I get a default value of '-- Select -- '
Why is the default value working for some items but coming up as 'select' for others?
ViewBag and ViewData is not the best option to use. If you want to refactor this code soon - it will be hard to track changes you should make.
Use it only if there is no any other option. However I cannot think about case like that.
Imagine that your model contains property SelectedItem of type SelectListItem and Items of type SelectList
Then use DrodownListFor
#Html.DropDownListFor(m => m.SelectedItem, Model.Items)
Yea don't use ViewBag and ViewData to pass around data if you don't have to. Use ViewModel instead because it's strongly-typed and you don't have to cast it on your view, and you can declare additional properties to suit your needs.
Again, I am not sure the relationship between your ItemInstance and the list of Items coming back from the database, but from what you're trying to do, I am guessing there is a dropdown on the side of your edit page, and whatever the current item instance being edited would be selected on the dropdown?
public class EditItemInstanceViewModel
{
public IEnumerable<ItemOptionViewModel> AvailableItemOptions { get; set; }
public ItemInstanceViewModel ItemInstance { get; set; }
}
public class ItemOptionViewModel
{
public int ItemId { get; set; }
public string ItemName { get; set; }
}
public class ItemInstanceViewModel
{
public int ItemInstanceId { get; set; }
public string ItemInstanceName { get; set; }
// ... there might be more properties
}
Then in your controller, you can fill EditItemInstanceViewModel like this:
public ActionResult EditItemInstance(int id)
{
ItemInstance itemInstance = db.ItemInstances.Find(id);
if (itemInstance == null)
{
return HttpNotFound();
}
var availableItemOptions = (from it in db.Items.Where(x => x.deleted == false)
select new ItemOptionViewModel
{
ItemID = it.ID,
ItemName = it.ItemID + ": " + it.Name
})
.OrderBy(x => x.ItemName)
.ToList();
var vm = new EditItemInstanceViewModel
{
AvailableItemOptions = availableItemOptions,
ItemInstance = new ItemInstanceViewModel
{
ItemInstanceId = itemInstance.Id,
ItemInstanceName = itemInstance.Name
}
};
return View(vm);
}
Then on the view:
#model EditItemInstanceViewModel
#{
}
...
#Html.DropdownList("selected-item-id",
<!-- Enumerable items; Dropdown value field; Dropdown text field; Selected value; -->
new SelectList(Model.AvailableItemOptions, "ItemInstanceId", "ItemInstanceName", Model.ItemInstance.ItemInstanceId),
"-- Select --",
new { #class = "form-control chosen-select" })
...
Nice and clean!
My View is bound to this model
public class HomepageViewModel
{
public HomepageViewModel()
{
Regions = new List<TMRegion>();
}
public List<TMRegion> Regions { get; set; }
public int SelectedRegion { get; set; }
public SelectList SelectListRegions
{
get
{
List<SelectListItem> items = new List<SelectListItem>();
foreach (var tmRegion in Regions)
{
items.Add(new SelectListItem()
{
Value = tmRegion.Value.ToString(),
Text = tmRegion.Display
});
}
return new SelectList(items);
}
}
}
My view is like this -
#model ProSell.Admin.Models.ViewModels.HomepageViewModel
#using (Html.BeginForm("Index", "Search", FormMethod.Post, new { viewModel = Model }))
{
#Html.HiddenFor(m=>m.Regions)
#Html.DropDownListFor(model => model.SelectedRegion, Model.SelectListRegions.Items as List<SelectListItem>, "Select a region")
<input type="submit"/>
}
My controller populates Regions like this -
// GET: Search
public async Task<ViewResult> Index(HomepageViewModel viewModel)
{
if (viewModel.Regions.Count == 0)
{
viewModel = new HomepageViewModel();
JavaScriptSerializer js = new JavaScriptSerializer();
viewModel.Regions =
js.Deserialize<TMRegion[]>(
await _ApiConsumer.ExecuteGetMethod("myurlinhere"))
.ToList();
}
return View(viewModel);
}
The Drop down is populated in the view. When i select a region and submit the HomepageViewModel has the SelectedRegion correctly set to whatever id was selected, but the collection of Regions is empty.
How do I maintain the list in the model on submit?
Generally you should attempt to reduce the amount of data the client sends back to the server especially with collections since it's far more efficient for the server to make a database call to retrieve the collection than have it passed back with the form data.
As Stephen said you can re-populate the collections Regions from your controller or if you need it when returning the view due to ModelState error you can add the population code into your model's constructor.
Also you can clean up your collection property like this:
public IEnumerable<SelectListItem> SelectListRegions
{
get
{
return Regions.Select(x => new SelectListItem
{
Text = x.Display,
Value = x.Value.ToString()
});
}
}
and in your View:
#Html.DropDownListFor(model => model.SelectedRegion, Model.SelectListRegions, "Select a region")
In this example ASP.Net MVC 4 program I have a user fill in details about a horse race. The race has a name a well as a list of horses involved. Each horse has a name and an age.
The form uses ajax and javascript to allow the person to add and delete horse input fields on the fly, which is then submitted all at once when the submit button is pressed.
To make this process easy for me, I'm using an html helper made by Matt Lunn.
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string htmlFieldName = null) where TModel : class
{
var items = expression.Compile()(html.ViewData.Model);
var sb = new StringBuilder();
if (String.IsNullOrEmpty(htmlFieldName))
{
var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression);
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = Guid.NewGuid().ToString();
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);
sb.Append(String.Format(#"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid));
sb.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
}
return new MvcHtmlString(sb.ToString());
}
While I don't understand all the details (please read the blog post), I do know that it changes the index values into guids rather than sequential integers. This allows me to delete items in the middle of the list without needing to recalculate indexes.
Here is the rest of my code for my MCVE
HomeController.cs
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
var model = new Race();
//start with one already filled in
model.HorsesInRace.Add(new Horse() { Name = "Scooby", Age = 10 });
return View(model);
}
[HttpPost]
public ActionResult Index(Race postedModel)
{
if (ModelState.IsValid)
//model is valid, redirect to another page
return RedirectToAction("ViewHorseListing");
else
//model is not valid, show the page again with validation errors
return View(postedModel);
}
[HttpGet]
public ActionResult AjaxMakeHorseEntry()
{
//new blank horse for ajax call
var model = new List<Horse>() { new Horse() };
return PartialView(model);
}
}
Models.cs
public class Race
{
public Race() { HorsesInRace = new List<Horse>(); }
[Display(Name = "Race Name"), Required]
public string RaceName { get; set; }
[Display(Name = "Horses In Race")]
public List<Horse> HorsesInRace { get; set; }
}
public class Horse
{
[Display(Name = "Horse's Name"), Required]
public string Name { get; set; }
[Display(Name = "Horse's Age"), Required]
public int Age { get; set; }
}
Index.cshtml
#model CollectionAjaxPosting.Models.Race
<h1>Race Details</h1>
#using (Html.BeginForm())
{
#Html.ValidationSummary()
<hr />
<div>
#Html.DisplayNameFor(x => x.RaceName)
#Html.EditorFor(x => x.RaceName)
#Html.ValidationMessageFor(x => x.RaceName)
</div>
<hr />
<div id="horse-listing">#Html.EditorForMany(x => x.HorsesInRace)</div>
<button id="btn-add-horse" type="button">Add New Horse</button>
<input type="submit" value="Enter Horses" />
}
<script type="text/javascript">
$(document).ready(function () {
//add button logic
$('#btn-add-horse').click(function () {
$.ajax({
url: '#Url.Action("AjaxMakeHorseEntry")',
cache: false,
method: 'GET',
success: function (html) {
$('#horse-listing').append(html);
}
})
});
//delete-horse buttons
$('#horse-listing').on('click', 'button.delete-horse', function () {
var horseEntryToRemove = $(this).closest('div.horse');
horseEntryToRemove.prev('input[type=hidden]').remove();
horseEntryToRemove.remove();
});
});
</script>
Views/Shared/EditorTemplates/Horse.cshtml
#model CollectionAjaxPosting.Models.Horse
<div class="horse">
<div>
#Html.DisplayNameFor(x => x.Name)
#Html.EditorFor(x => x.Name)
#Html.ValidationMessageFor(x => x.Name)
</div>
<div>
#Html.DisplayNameFor(x => x.Age)
#Html.EditorFor(x => x.Age)
#Html.ValidationMessageFor(x => x.Age)
</div>
<button type="button" class="delete-horse">Remove Horse</button>
<hr />
</div>
Views/Home/AjaxMakeHorseEntry.cshtml
#model IEnumerable<CollectionAjaxPosting.Models.Horse>
#Html.EditorForMany(x => x, "HorsesInRace")
The data flow works with this code. A person is able to create and delete horse entries as much as they want on the page, and when the form is submitted all entered values are given to the action method.
However, if the user does not enter in the [Required] information on a horse entry, ModelState.IsValid will be false showing the form again, but no validation messages will be shown for the Horse properties. The validation error do show up in the ValidationSummary list though.
For example, if Race Name is left blank, along with one Horse's Name, a validation message will be shown for the former. The latter will have a validation <span> with the class "field-validation-valid".
I'm very sure this is caused because the EditorForMany method creates new guids for each property each time the page is created, so validation messages can't be matched to the correct field.
What can I do to fix this? Do I need to abandon guid index creation or can an alteration be made to the EditorForMany method to allow validation messages to be passed along correctly?
I'm very sure this is caused because the EditorForMany method creates new guids for each property each time the page is created, so validation messages can't be matched to the correct field.
Yep; that's exactly what is happening here.
To fix this, we need to amend EditorForMany() so that it re-uses the GUID for an item, rather than generating a new one. In turn, this means we need to track what GUID has been assigned to what item, so that it can be re-used.
The former can be accomplished by internal modifications to EditorForMany(). The latter requires us to:
Add a property to our models in which the assigned GUID can be stored
Add a parameter to EditorForMany() to tell the helper which property contains the GUID to re-use (if any).
This leaves the EditorForMany helper looking like this;
public static class HtmlHelperExtensions
{
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, string htmlFieldName = null) where TModel : class
{
htmlFieldName = htmlFieldName ?? ExpressionHelper.GetExpressionText(propertyExpression);
var items = propertyExpression.Compile()(html.ViewData.Model);
var htmlBuilder = new StringBuilder();
var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName);
Func<TValue, string> indexResolver = null;
if (indexResolverExpression == null)
{
indexResolver = x => null;
}
else
{
indexResolver = indexResolverExpression.Compile();
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = indexResolver(item);
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters);
if (String.IsNullOrEmpty(guid))
{
guid = Guid.NewGuid().ToString();
}
else
{
guid = html.AttributeEncode(guid);
}
htmlBuilder.Append(String.Format(#"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldNameWithPrefix, guid));
if (indexResolverExpression != null)
{
htmlBuilder.Append(String.Format(#"<input type=""hidden"" name=""{0}[{1}].{2}"" value=""{1}"" />", htmlFieldNameWithPrefix, guid, ExpressionHelper.GetExpressionText(indexResolverExpression)));
}
htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
}
return new MvcHtmlString(htmlBuilder.ToString());
}
}
We also then need to change our models, to add a property in which the GUID is stored;
public class Race
{
public Race() { HorsesInRace = new List<Horse>(); }
[Display(Name = "Race Name"), Required]
public string RaceName { get; set; }
[Display(Name = "Horses In Race")]
public List<Horse> HorsesInRace { get; set; }
}
public class Horse
{
[Display(Name = "Horse's Name"), Required]
public string Name { get; set; }
[Display(Name = "Horse's Age"), Required]
public int Age { get; set; }
// Note the addition of Index here.
public string Index { get; set; }
}
... and lastly, change our usage of EditorForMany() to use the new signature;
Index.cshtml;
<div id="horse-listing">#Html.EditorForMany(x => x.HorsesInRace, x => x.Index)</div>
AjaxMakeHorseEntry.cshtml;
#Html.EditorForMany(x => x, x => x.Index, "HorsesInRace")
... which should then make the validation messages appear.
As an aside, I recommend not using the htmlFieldName parameter for EditorForMany, and instead changing your controller action to;
[HttpGet]
public ActionResult AjaxMakeHorseEntry()
{
var model = new Race();
model.HorsesInRace.Add(new Horse());
return PartialView(model);
}
... then your AjaxMakeHorseEntry.cshtml view to be just;
#model Models.Race
#Html.EditorForMany(x => x.HorsesInRace, x => x.Index)
Otherwise, the generated name attributes break when nesting usage of EditorForMany().
I'm going to update the blog post to use the above version of EditorForMany(), less the acceptance of the htmlFieldName parameter, for this reason.
I want to validate values in DropDownListFor but its not working. I have included required script files for client side validation too.
In ViewModel:
public class SearchViewModel
{
public List<MarriageProfile> Profiles { get; set; }
public SearchProfile SearchProfile { get; set; }
}
public class SearchProfile
{
[Required(ErrorMessage="*")]
public SelectList Ages { get; set; }
public string Age { get; set; }
}
In Controller:
var ages = AgeList
.Select(p => new SelectListItem()
{
Text = p.ToString(),
Value = p.ToString()
}).ToList();
SearchViewModel.SearchProfile.Ages = new SelectList(ages, "Text", "Value");
static List<string> AgeList = new List<string>()
{
"18-23",
"23-28",
"28-33",
"33-38",
"38-43",
"43-48"
};
In View:
#Html.DropDownListFor(model => model.Age, Model.Ages, "--Select One--", new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Age)
On submitting the form, no client or server side validation is working for the dropdownlist above. What could be wrong with the code?
It appears you need to tag the Age property with the Required attribute and not the Ages list. Check out this answer: ASP.NET MVC 3 and validation attribute for dropdownlist with default value of 0
I am trying to build a Dropdownlist, but battling with the Html.DropDownList rendering.
I have a class:
public class AccountTransactionView
{
public IEnumerable<SelectListItem> Accounts { get; set; }
public int SelectedAccountId { get; set; }
}
That is basically my view model for now. The list of Accounts, and a property for returning the selected item.
In my controller, I get the data ready like this:
public ActionResult AccountTransaction(AccountTransactionView model)
{
List<AccountDto> accounts = Services.AccountServices.GetAccounts(false);
AccountTransactionView v = new AccountTransactionView
{
Accounts = (from a in accounts
select new SelectListItem
{
Text = a.Description,
Value = a.AccountId.ToString(),
Selected = false
}),
};
return View(model);
}
Now the problem:
I am then trying to build the Drop down in my view:
<%=Html.DropDownList("SelectedAccountId", Model.Accounts) %>
I am getting the following error:
The ViewData item that has the key 'SelectedAccountId' is of type 'System.Int32' but must be of type 'IEnumerable'.
Why would it want me to return the whole list of items? I just want the selected value. How should I be doing this?
You have a view model to which your view is strongly typed => use strongly typed helpers:
<%= Html.DropDownListFor(
x => x.SelectedAccountId,
new SelectList(Model.Accounts, "Value", "Text")
) %>
Also notice that I use a SelectList for the second argument.
And in your controller action you were returning the view model passed as argument and not the one you constructed inside the action which had the Accounts property correctly setup so this could be problematic. I've cleaned it a bit:
public ActionResult AccountTransaction()
{
var accounts = Services.AccountServices.GetAccounts(false);
var viewModel = new AccountTransactionView
{
Accounts = accounts.Select(a => new SelectListItem
{
Text = a.Description,
Value = a.AccountId.ToString()
})
};
return View(viewModel);
}
Step-1: Your Model class
public class RechargeMobileViewModel
{
public string CustomerFullName { get; set; }
public string TelecomSubscriber { get; set; }
public int TotalAmount { get; set; }
public string MobileNumber { get; set; }
public int Month { get; set; }
public List<SelectListItem> getAllDaysList { get; set; }
// Define the list which you have to show in Drop down List
public List<SelectListItem> getAllWeekDaysList()
{
List<SelectListItem> myList = new List<SelectListItem>();
var data = new[]{
new SelectListItem{ Value="1",Text="Monday"},
new SelectListItem{ Value="2",Text="Tuesday"},
new SelectListItem{ Value="3",Text="Wednesday"},
new SelectListItem{ Value="4",Text="Thrusday"},
new SelectListItem{ Value="5",Text="Friday"},
new SelectListItem{ Value="6",Text="Saturday"},
new SelectListItem{ Value="7",Text="Sunday"},
};
myList = data.ToList();
return myList;
}
}
Step-2: Call this method to fill Drop down in your controller Action
namespace MvcVariousApplication.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
RechargeMobileViewModel objModel = new RechargeMobileViewModel();
objModel.getAllDaysList = objModel.getAllWeekDaysList();
return View(objModel);
}
}
}
Step-3: Fill your Drop-Down List of View as follows
#model MvcVariousApplication.Models.RechargeMobileViewModel
#{
ViewBag.Title = "Contact";
}
#Html.LabelFor(model=> model.CustomerFullName)
#Html.TextBoxFor(model => model.CustomerFullName)
#Html.LabelFor(model => model.MobileNumber)
#Html.TextBoxFor(model => model.MobileNumber)
#Html.LabelFor(model => model.TelecomSubscriber)
#Html.TextBoxFor(model => model.TelecomSubscriber)
#Html.LabelFor(model => model.TotalAmount)
#Html.TextBoxFor(model => model.TotalAmount)
#Html.LabelFor(model => model.Month)
#Html.DropDownListFor(model => model.Month, new SelectList(Model.getAllDaysList, "Value", "Text"), "-Select Day-")