This is something that has always puzzled me as to the best way round, while keeping maintainable code. The below code sets up a list of months and years for a payment gateway form, before assigning these to a variable of type List<SelectListItem>.
Intial Action
PayNowViewModel paymentGateway = new PayNowViewModel();
List<SelectListItem> paymentGatewayMonthsList = new List<SelectListItem>();
List<SelectListItem> paymentGatewayYearsList = new List<SelectListItem>();
for (int i = 1; i <= 12; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayMonthsList.Add(selectListItem);
}
int year = DateTime.Now.Year;
for (int i = year; i <= year + 10; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayYearsList.Add(selectListItem);
}
paymentGateway.ExpiryMonth = paymentGatewayMonthsList;
paymentGateway.ExpiryYear = paymentGatewayYearsList;
return View(paymentGateway);
It's a fair bit of code, and I find myself repeating this code, in similar formats to re-setup the dropdown lists options should the ModelState.IsValid be false and I want to return back to the view for the user to correct there mistakes.
HttpPost Action - Code
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ConfirmPayment(PayNowViewModel paymentGatewayForm, FormCollection form)
{
if (ModelState.IsValid)
{
// Post processing actions...
return View();
}
else
{
for (int i = 1; i <= 12; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayMonthsList.Add(selectListItem);
}
int year = DateTime.Now.Year;
for (int i = year; i <= year + 10; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayYearsList.Add(selectListItem);
}
form.ExpiryMonth = paymentGatewayMonthsList;
form.ExpiryYear = paymentGatewayYearsList;
return View("MakePayment", form);
}
}
What's the best way to centralise this dropdown setup code so its only in one place? At present you'll see a large proportion (the for loops), is exactly repeated twice. A base controller with function? Or is it better to re-setup like the above?
Any advice appreciated!
Mike.
Add a private method to your controller (the following code assumes your ExpiryMonth and ExpiryYear properties are IEnumerable<SelectListItem> which is all that the DropDownListFor() method requires)
private void ConfigureViewModel(PayNowViewModel model)
{
model.ExpiryMonth = Enumerable.Range(1, 12).Select(m => new SelectListItem
{
Value = m.ToString(),
Text = m.ToString("00")
});
model.ExpiryYear = Enumerable.Range(DateTime.Today.Year, 10).Select(y => new SelectListItem
{
Value = y.ToString(),
Text = y.ToString("00")
});
}
and then in the GET method
public ActionResult ConfirmPayment()
{
PayNowViewModel model = new PayNowViewModel();
ConfigureViewModel(model);
return View(model);
}
and in the POST method
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ConfirmPayment(PayNowViewModel model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
.... // save and redirect (should not be returning the view here)
}
If the set of your dropdown options is fixed (or recompilation is OK after the potential options change), you can use an enum to store your options.
public enum Month {
// if the dropdown is not required, add default value 0
Optional = 0,
[Display(Name = #"Month_January")]
January = 1,
[Display(Name = #"Month_February")]
February = 2,
// etc ..
}
To render this as a dropdown use an EditorTemplate Enum.cshtml:
#model Enum
#{
var enumType = ViewData.ModelMetadata.ModelType;
var allValues = Enum.GetValues(enumType).Cast<object>().ToSelectList(Model);
// read any attributes like [Required] from ViewData and ModelMetadata ...
var attributes = new Dictionary<string, object>();
}
#Html.DropDownListFor(m => m, allValues, attributes)
The ToSelectList extension method loops over all enum values and converts them to SelectListItems:
public static IList<SelectListItem> ToSelectList<T>(this IEnumerable<T> list) {
return ToSelectList<T>(list, list.FirstOrDefault());
}
public static IList<SelectListItem> ToSelectList<T>(this IEnumerable<T> list, T selectedItem) {
var items = new List<SelectListItem>();
var displayAttributeType = typeof(DisplayAttribute);
foreach (var item in list) {
string displayName;
// multi-language:
// assume item is an enum value
var field = item.GetType().GetField(item.ToString());
try {
// read [Display(Name = #"someKey")] attribute
var attrs = (DisplayAttribute)field.GetCustomAttributes(displayAttributeType, false).First();
// lookup translation for someKey in the Resource file
displayName = Resources.ResourceManager.GetString(attrs.Name);
} catch {
// no attribute -> display enum value name
displayName = item.ToString();
}
// keep selected value after postback:
// assume selectedItem is the Model passed from MVC
var isSelected = false;
if (selectedItem != null) {
isSelected = (selectedItem.ToString() == item.ToString());
}
items.Add(new SelectListItem {
Selected = isSelected,
Text = displayName,
Value = item.ToString()
});
}
return items;
}
To support multiple languages, add translations for the display name keys, e.g. "Month_January", to the Resource file.
Now that the setup code has been abstracted away using some reflection magic, creating a new viewmodel is a breeze :>
public class PayNowViewModel {
// SelectListItems are only generated if this gets rendered
public Month ExpiryMonth { get; set; }
}
// Intial Action
var paymentGateway = new PayNowViewModel();
return View(paymentGateway);
// Razor View: call the EditorTemplate
#Html.EditorFor(m => m.ExpiryMonth)
Note that in the EditorTemplate, Model is passed as the selected item to ToSelectList. After postback, Model will hold the currently selected value. Therefore it stays selected, even if you just return the model after an error in the controller:
// HttpPost Action
if (!ModelState.IsValid) {
return View("MakePayment", paymentGatewayForm);
}
Took us some time to come up with this solution, credits go to the Saratiba team.
Related
in this project i create cardGroup. in httpGet Method we get some needed info and pass to view to fill dropdown. when httpPost trigger if some field Date has Problem we must return error with addModelError but after return View, all ViewData Clear and Return Exception. how can handle this. just show error in view.
[HttpGet]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup()
{
var discounts =
UnitOfWork.DiscountPatternRepository.GetNotExpireDiscountPattern();
var discountDtos = discounts?.Select(c => new SelectListItem
{
Text = c.PatternTitle,
Value = c.Id.ToString()
}).ToList();
ViewData["DiscountPatterns"] = discountDtos;
var serials =
UnitOfWork.ChargeCardSerialRepository.GetNotAssignedSerials();
var serialDtos = serials?.Select(c => new SelectListItem
{
Text = c.SerialNumber.ToString(),
Value = c.Id.ToString()
}).ToList();
ViewData["ChargeSerials"] = serialDtos;
ViewData["CardSerialCount"] =
UnitOfWork.GiftCardSerialRepository.GetNotUsedGiftSerials();
return View();
}
[HttpPost]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup(CardGroupCreateDto dto)
{
if (!ModelState.IsValid)
return View(dto);
if(!UnitOfWork.DiscountPatternRepository
.IsCardGroupDateInRange(dto.DiscountPatternId,
dto.ActiveFromDate, dto.ActiveToDate))
{
ModelState.AddModelError("ActiveFromDate", #"Error In Date.");
return View(dto); <---Problem Here
}
var group = dto.LoadFrom();
var insertedId = UnitOfWork.CardGroupRepository.Add(group);
foreach (var rangeDto in group.CardGroupGiftSerialRanges)
{
for (var i = rangeDto.GiftCardSerialBegin; i <=
rangeDto.GiftCardSerialEnd; i++)
{
var serial =
UnitOfWork.GiftCardSerialRepository.GetBySerial(i);
if (serial != null)
{
serial.CardGroupGiftSerialRangeId = rangeDto.Id;
serial.DiscountPatternId = group.DiscountPatternId;
UnitOfWork.Complete();
}
}
}
return Redirect("/CardGroup");
}
From this article:
ViewData
ViewData is a property of ControllerBase class.
ViewData is used to pass data from controller to corresponding view
Its life lies only during the current request. If redirection occurs, then its value becomes null. It’s required typecasting for getting data and check for null values to avoid error.
So what's happening is once you've done your post back to the server, you're now in a different request, meaning, that you need to repopulate your ViewData items so that their values are populated again, or else they'll be null.
So I'd recommend refactoring your Dropdown population method into a private method on your controller and then call that method in your post when you find a validation error or are just returning by calling return View(dto).
If they're used in other controllers, you can add them to a LookupService or LookupRepository or even a general helpers class that contains your lookup logic (whatever fits into your UnitofWork pattern the best for you), to make them available to those other controllers, instead of having it as a private method as per my example.
So something like this for example:
[HttpGet]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup()
{
PopulateCreateCardGroupLookups();
return View();
}
[HttpPost]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup(CardGroupCreateDto dto)
{
if (!ModelState.IsValid)
{
PopulateCreateCardGroupLookups();
return View(dto);
}
if(!UnitOfWork.DiscountPatternRepository
.IsCardGroupDateInRange(dto.DiscountPatternId,
dto.ActiveFromDate, dto.ActiveToDate))
{
ModelState.AddModelError("ActiveFromDate", #"Error In Date.");
PopulateCreateCardGroupLookups();
return View(dto); <---Problem Here
}
var group = dto.LoadFrom();
var insertedId = UnitOfWork.CardGroupRepository.Add(group);
foreach (var rangeDto in group.CardGroupGiftSerialRanges)
{
for (var i = rangeDto.GiftCardSerialBegin; i <=
rangeDto.GiftCardSerialEnd; i++)
{
var serial =
UnitOfWork.GiftCardSerialRepository.GetBySerial(i);
if (serial != null)
{
serial.CardGroupGiftSerialRangeId = rangeDto.Id;
serial.DiscountPatternId = group.DiscountPatternId;
UnitOfWork.Complete();
}
}
}
return Redirect("/CardGroup");
}
private void PopulateCreateCardGroupLookups()
{
var discounts =
UnitOfWork.DiscountPatternRepository.GetNotExpireDiscountPattern();
var discountDtos = discounts?.Select(c => new SelectListItem
{
Text = c.PatternTitle,
Value = c.Id.ToString()
}).ToList();
ViewData["DiscountPatterns"] = discountDtos;
var serials =
UnitOfWork.ChargeCardSerialRepository.GetNotAssignedSerials();
var serialDtos = serials?.Select(c => new SelectListItem
{
Text = c.SerialNumber.ToString(),
Value = c.Id.ToString()
}).ToList();
ViewData["ChargeSerials"] = serialDtos;
ViewData["CardSerialCount"] =
UnitOfWork.GiftCardSerialRepository.GetNotUsedGiftSerials();
}
I have page which contains multiple dropdowns with option like
<option>1<option>
<option>2<option>
<option>3<option>
and
<option>-5<option>
<option>-6<option>
<option>-7<option>
so i have created in a function to generate dropdown options in razor view.
#functions {
public List<SelectListItem> GenerateDropDown(int startvalue, int endValue)
{
var dropDownList = new List<SelectListItem>();
for (int i = startvalue; i <= endValue; i++)
{
string val = i.ToString();
dropDownList.Add(new SelectListItem { Text = val, Value = val });
}
return dropDownList;
}
}
and using like this
#Html.DropDownListFor(m => m.xyz, GenerateDropDown(1, 10))
#Html.DropDownListFor(m => m.Abc, GenerateDropDown(2, 20))
this work fine but i want use the same function in multiple pages with out code duplication i tried using helper method but no use can any one suggest me how to centralize GenerateDropDown function.
Create Static Class, with static method GenerateDropDown in it.
Let's say
public static class GeneratorHelper{
public static List<SelectListItem> GenerateDropDown(int startvalue, int endValue)
{
var dropDownList = new List<SelectListItem>();
for (int i = startvalue; i <= endValue; i++)
{
string val = i.ToString();
dropDownList.Add(new SelectListItem { Text = val, Value = val });
}
return dropDownList;
}
}
and now in the razor you just use the class as:
GeneratorHelper.GenerateDropDown(1,5);
I am trying to populate the selected value field of an MVC drop down list, but I have been unsuccessful. I am using MVC 5.1.
Here is a screenshot of the selected value and the model showing sales receipts:
Instead of sales receipts being selected, the model defaults to estimates:
When I use ViewData, the drop down works successfully:
Html.DropDownList("JMASettings.InvoiceMode")
Here is my code:
public void GetInvoiceMode(JMASettings model)
{
Dictionary<string, string> modes = new Dictionary<string, string>();
modes.Add("Estimates", "Estimates");
modes.Add("Invoices Only (Payments If Paid)", "Invoices");
modes.Add("Invoices No Payments", "InvoicesNoPayments");
modes.Add("Sales Receipts", "Sales Receipts");
if (Session["dataSource"] != null && Session["dataSource"].ToString() == "QBD")
modes.Add("Sales Orders", "Sales Orders");
IList<SelectListItem> ilSelectList = new List<SelectListItem>();
foreach (KeyValuePair<string, string> mode in modes)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Text = mode.Key;
selectListItem.Value = mode.Value;
if (model.InvoiceMode == mode.Value)
selectListItem.Selected = true;
else if (String.IsNullOrEmpty(model.InvoiceMode) && mode.Key == "Sales Receipts")
selectListItem.Selected = true;
ilSelectList.Add(selectListItem);
}
ViewData["JMASettings.InvoiceMode"] = ilSelectList;
}
Keep the property which represents the selected item on your view model like this.
public class CreateOrderVM
{
public List<SelectListItem> Modes {set;get;}
public string SelectedMode {set;get;}
public CreateOrderVM()
{
this.Modes=new List<SelectListItem>();
}
}
Now when you have to render the view, load the Modes collection and set the SelectedMode to whatever you want
public ActionResult Show()
{
var vm=new CreateOrderVM();
vm.Modes = GetModes();
//Set one of them as selected
vm.SelectedMode="B";
return View(vm);
}
private List<SelectListItem> GetModes()
{
var list=new List<SelectListItem>();
//hard coded for demo.Replace with actual values
list.Add(new SelectListItem { Value="A", Text="A"});
list.Add(new SelectListItem { Value="B", Text="B"});
list.Add(new SelectListItem { Value="C", Text="C"});
return list;
}
In your view,
#model YourNamespace.CreateOrderVM
#Html.DropdownListFor(s=>s.SelectedMode,Model.Modes,"Select")
This question already has answers here:
NullReferenceException after POST
(2 answers)
Closed 9 years ago.
I've got a form that has a dropDownlist using the Model to fill the list, the view is rendered. The issue is that when i press the submit button, a null pointer exception for Model is thrown. I want to receive the value selected in the Post Action.
Here is my code:
Model:
public class BillViewModel
{
public List<SelectListItem> ClientList { get; set; }
public int SelectedClient { get; set; }
}
Controller:
public ActionResult Index()
{
var billRepo = new BillRepo();
var bill = new BillViewModel {ListProducts = billRepo.GetAllProducts()};
bill.ClientList = new List<SelectListItem>();
List<Client> allClientList = billRepo.GetAllClients();
foreach (Client client in allClientList)
{
var item = new SelectListItem() { Value = client.ClientId.ToString(), Text = client.Name };
bill.ClientList.Add(item);
}
ViewBag.ClientSelect = new SelectList(billRepo.GetAllClients(), "value", "text", bill.SelectedClient);
bill.SelectedClient = 1;
return View(bill);
}
[HttpPost]
public ActionResult Index(BillViewModel billViewModel)
{
return View();
}
View: The Model
#using (Html.BeginForm())
{
#Html.DropDownListFor(item => item.SelectedClient, Model.ClientList, "Select Client")
<input type="submit" value="Aceptar"/>
}
In your POST action you are returning the same Index view as in your GET action. But you are not passing any model to this view. That's why you are getting a NRE. Your view must render a dropdown and you need to populate its values, the same way you did in your GET action:
[HttpPost]
public ActionResult Index(BillViewModel billViewModel)
{
bill.ClientList = billRepo
.GetAllClients()
.ToList()
.Select(x => new SelectListItem
{
Value = client.ClientId.ToString(),
Text = client.Name
})
.ToList();
return View(billViewModel);
}
Notice how the view model is passed to the view and how the ClientList property (to which your dropdown is bound) is filed with values.
I need the value of SelectListItem to be int.
So I pull it of the database, convert it to string in the process and store to listitem's value.
public class BookAdd
{
public BookAdd()
{
public Book Book { get; set; }
DataModelContainer db = new DataModelContainer();
IEnumerable<SelectListItem> items = db.PublisherSet
.Select(i => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)i.Id),
Text = i.Name
});
}
}
I then need to store the value again as int to Book.PublisherId when selected from dropdownlist. I know the code below is not complete, I figured I need somehow convert the selected item's value to int, how do I do it?
#model Bookshops.Models.BookAdd
...
#Html.DropDownListFor(model => model.Book.PublisherId, Model.items);
And finaly controler:
public ActionResult Create2()
{
var bookAdd = new BookAdd();
ViewBag.Publisher = bookAdd.items;
return View(bookAdd);
}
//
// POST: /Book/Create
[HttpPost]
public ActionResult Create2(BookAdd book)
{
if (ModelState.IsValid)
{
Book book2 = new Book();
book2.Id = book.Book.Id;
book2.AuthorId = book.Book.AuthorId;
book2.Isbn = book.Book.Isbn;
book2.Id = int.Parse(book.Book.PubId);
book2.Title = book.Book.Title;
book2.YearPublished = book.Book.YearPublished;
db.BookSet.Add(book2);
db.SaveChanges();
}
return RedirectToAction("Index");
}
I'm using MVC 3, EF 4.
Instead of building a SelectListItem, I built something like this:
public struct BookItem
{
int id;
string name;
}
Then add the following item to the model and select data into it
IEnumerable<BookItem> bookList {get; set;}
Fianally
#Html.DropDownListFor(model => model.Book.PublicherId, new SelectList(Model.bookList, "id", "name"))
When the selected Item of the DropDownList will get posted back convert it to int from string like this Convert.ToInt32(DropDownSelectedItem) or with Int32.TryParse(value, out number)
to start you can simplify the mapping code to
db
.PublisherSet
.Select(x=> new SelectListItem{Value = x.Id.ToString(), Text = x.Name})
.ToArray();
In the view you can then do
#Html.DropDownListFor(model => model.Book.PublisherId, Model.items);
and it will properly render a select element with the options and the appropriate publisher selected.
You can then use the model binding conventions to pass this back to the controller action when the user clicks the submit button.
In the end, it's all text. that's the nature of the web. we have a lot of tricks, converters and binders to turn string dictionaries into rich view models, but ultimately it's just strings.