I have a class that I populate when a user navigates to a certain page, /Home/About, within my MVC4 application. I populate a class with data and I would like to have in my view that data in a drop down list.
My class looks like this: (UPDATED)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
public class WorkSection : List<WorkSection>
{
[Required]
[Display(Name = "WorkSection")]
public int ID { get; set; }
public string Code { get; set; }
public SelectList WorkSections { get; set; }
public WorkSection()
{
// Default Constructor
}
public WorkSection(int id, string code)
{
this.ID = ws_id;
this.Code = code;
}
}
How do I take this populated List of type WorkSection and make that the data source for my drop down list? I would like to display the Code and Source field in a concatenated fashion, like "Code:Source" within the drop down list with the ID as the selected item's value.
UPDATE for ActionResult, where code will be called from on /Home/About
public ActionResult About()
{
WorkSection model = new WorkSection();
OracleConnection con = new OracleConnection();
con.ConnectionString = "omitted";
try
{
con.Open();
}
catch (Exception ex)
{
throw ex;
}
try
{
OracleDataReader reader = null;
// Work Section
OracleCommand cmd = new OracleCommand("SELECT ID, CODE FROM MyTable ORDER BY CODE", con);
reader = cmd.ExecuteReader();
while (reader.Read())
{
model.Add(new WorkSection()
{
ID = Int16.Parse(reader["ID"].ToString()),
Code = reader["CODE"].ToString()
});
}
model.WorkSections = BuildSelectList(model.WorkSections, m => m.ID, m => m.Code);
con.Close();
con.Dispose();
}
catch (Exception ex)
{
throw ex;
}
return View(model);
}
First up, we need a view model to encapsulate the data for the view:
public class TestViewModel
{
[Required]
[Display(Name = "Work section")]
// This represents the selected ID on the dropdown
public int WorkSectionId { get; set; }
// The dropdown itself
public SelectList WorkSections { get; set; }
// other properties
}
Next up, we need a way to populate the SelectList. I wrote a custom method a while ago to do just that:
private SelectList BuildSelectList<TSource>(IEnumerable<TSource> source,
Expression<Func<TSource, int>> valueKey, Expression<Func<TSource, string>> textKey,
object selectedValue = null)
{
var selectedValueKey = ((MemberExpression)(MemberExpression)valueKey.Body).Member.Name;
var selectedTextKey = ((MemberExpression)(MemberExpression)textKey.Body).Member.Name;
return new SelectList(source, selectedValueKey, selectedTextKey, selectedValue);
}
This uses expression trees for type-safety, ensuring problems are caught at compile-time, rather than run-time. SelectList also uses one property for the text key and one for the value key. In your situation, this obviously creates a problem, because you want to combine Code and Source to form the text key. In order to get around that, you'll need to create a new property in WorkSection that combines both:
public string CodeSource
{
get { return this.Code + ":" + this.Source; }
}
That way, you can use that to create the SelectList as normal. To do that, your action might like something like:
public ActionResult Index()
{
var workSections = // ... fetch from database
TestViewModel model = new TestViewModel();
model.WorkSections = BuildSelectList(workSections, m => m.ID, m => m.CodeSource);
return View(model);
}
You can use that in the view like so:
#Html.DropDownListFor(m => m.WorkSectionId, Model.WorkSections, "--Please Select--")
#Html.ValidationMessageFor(m => m.WorkSectionId)
One final note on BuildSelectList. The method has saved me a lot of time when dealing with dropdowns in general. So much so that I now define it as a public method on a base controller, which I then derive all of my controllers from. However, if you want to do that, you'll want to mark it with the [NonAction] attribute so it doesn't interfere with routing.
Update per comments
public class BaseController : Controller
{
[NonAction]
public SelectList BuildSelectList<TSource>(IEnumerable<TSource> source,
Expression<Func<TSource, int>> valueKey, Expression<Func<TSource, string>> textKey,
object selectedValue = null)
{
var selectedValueKey = ((MemberExpression)(MemberExpression)valueKey.Body).Member.Name;
var selectedTextKey = ((MemberExpression)(MemberExpression)textKey.Body).Member.Name;
return new SelectList(source, selectedValueKey, selectedTextKey, selectedValue);
}
}
Then you'd derive your controllers from BaseController instead:
public HomeController : BaseController
{
//
}
#Hmtl.DropdownListFor(m=>m.YourNameForSelectedWorkSectionId, Model.WorkSections.Select(x => new SelectListItem { Text = x.Code +":"+x.Source, Value = x.ID}))
Related
so i am following MS' example here:
https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/complex-data-model?view=aspnetcore-6.0&tabs=visual-studio
in short, i implemented a variant of the code they posted:
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData InstructorData { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
IQueryable<Instructor> Instructor = from s in _context.Instructor
select s;
// bunch of code for sorting and crap.
var pageSize = Configuration.GetValue("PageSize", 150);
InstructorData.Instructors = await PaginatedList<Instructor>.CreateAsync(
InstructorsIQ
//.AsNoTracking()
, pageIndex ?? 1, 150);
if (id != null)
{
InstructorID = id.Value;
var selectedInstructor = InstructorData.Instructors.SingleOrDefault(i => id.Value == i.ID);
InstructorData.Courses = instructor.Courses;
}
if (CourseID != null)
{
CourseID = AbID.Value;
var selectedCourse = InstructorData.Courses.SingleOrDefault(i => i.CourseID == CourseIDId);
await _context.Entry(selectedCourse).Collection(x => x.Enrollment).LoadAsync();
InstructorData.Enrollment = selectedCourse.Pitches;
}
}
}
}
where mine is much cooler because it can sort and all these other things.
one question i had is about the model variable
public InstructorIndexData InstructorData { get; set; }
i thought if instantiated this variable outside OnGetAsync
public InstructorIndexData InstructorData { get; set; } = new InstructorIndexData();
and commented out the corresponding new call at the beginning of the function, that i would be able to 'store' the contents across function calls.
it turns out this is not the case.
i need to use FromSqlRaw for some of the queries i'm running, but i want to avoid doing that if the previous selected value is the same as the current one.
in the example above, it would be something like choosing the same instructor, or course taught by them
is there any specific reason why InstructorData.Courses would be null on the next call to OnGetAsync? is there something wrong with how i'm modeling?
from what i can see all the foreign keys and relationships are set up correctly.
i admit i'm new to this whole situation and could be making a super simple mistake.
i just don't want to run queries again if the previous one was the same selection.
I want to use the list of state controller in a district controller. Are there any better ideas.
I have tried one which is working
I put this code in the district controller by using constructor injection.
In this case, the entire code needs to be placed in the district controller.
Is there any way to reduce the code. A better way?
#region StateDropDown
public List<SelectListItem> StateDropDown()
{
List<SelectListItem> selectListItem = new List<SelectListItem>();
List<StateViewModel> stateList = Mapper.Map<List<State>, List<StateViewModel>>(_stateBusiness.GetStateForSelectList());
if (stateList != null)
foreach (StateViewModel state in stateList)
{
selectListItem.Add(new SelectListItem
{
Text = state.Description,
Value = state.Code.ToString(),
Selected = false
});
}
return selectListItem;
}
#endregion StateDropDown
This is what the term 'reusability' is invented for. Place the code in another file and make calls to it from any number of controllers you want, like code below.
//StateBusiness.cs
public class StateBusiness
{
public List<SelectListItem> GetStatesForDropdown()
{
//your logic here
return new List<SelectListItem>();
}
}
//StateController.cs
public class StateController : Controller
{
var state = new StateBusiness();
public ActionResult Index()
{
//call your code here
var states = state.GetStatesForDropdown();
//and do whatever you want
ViewBag.states = states;
return View();
}
}
//DistrictController.cs
public class DistrictController : Controller
{
var state = new StateBusiness();
public ActionResult Index()
{
//call it from here just the same
var states = state.GetStatesForDropdown();
ViewBag.states = states;
return View();
}
}
I don't know about better, but you could shorten this considerably using linq Select.
Mapper.Map<List<State>, List<StateViewModel>>(_stateBusiness.GetStateForSelectList())?
.Select(state => new SelectListItem
{
Text = state.Description,
Value = state.Code.ToString(),
Selected = false
}))?.ToList() ?? List<SelectListItem>();
If you using core one option might be to keep this off the controller and use a TagHelper this will let you inject the options into the tag with a simple attribute state-items reducing controller dependencies and keeping this state off the ViewBag while being more reusable.
Here is how it wold look in the view:
<select asp-for="State" state-items />
The TagHelper:
[HtmlTargetElement("select", Attributes = "state-items")]
public class StateItemsTagHelper : TagHelper {
private readonly StateBusiness _stateBusiness;
[HtmlAttributeName("asp-for")]
public ModelExpression For { get; set; }
public StateItemsTagHelper(StateBusiness stateBusiness) {
this._stateBusiness = stateBusiness;
}
public override void Process(TagHelperContext context, TagHelperOutput output) {
content.TagMode = TagMode.StartTagAndEndTag;
var value = For?.Model as string;
var items = _stateBusiness.GetStateForSelectList()?.Select(state => new SelectListItem {
Text = state.Description,
Value = state.Code.ToString(),
Selected = value == state.Code.ToString()
})) ?? Enumerable.Empty<SelectListItem>();
foreach(var item in items) {
output.Content.AppendHtml(item.ToHtmlContent());
}
}
}
For reusability item.ToHtmlContent is an extension method:
public static IHtmlContent ToHtmlContent(this SelectListItem item) {
var option = new TagBuilder("option");
option.Attributes.Add("value", item.Value);
if(item.Selected) {
option.Attributes.Add("selected", "selected");
}
option.InnerHtml.Append(item.Text);
return option;
}
Ive just imlemented typeahead functionality using thw Typeahead.js for MVC 5 Models wrapper
http://timdwilson.github.io/typeahead-mvc-model/
it all works ok but i just cant figure out how to set the limit on the number of items displayed in the suggestion drop down. the javascript would be this
$('#scrollable-dropdown-menu .typeahead').typeahead(null, {
name: 'countries',
limit: 10, -----> limit set here
source: countries
});
but I cant see how the mvc models wrapper implements this, there are three overloads and one of the has 'AdditionalViewdata' maybe this is whats needed ? There is no documentation that I can find and no one else seems to have done this (it looks like it defaults to 5 in the dropdown) My backend mechanism IS returning more than 5 results, its just not being reflected in the html
#Html.AutocompleteFor(model => model.Organisation.Org, model => model.Organisation.ORGID, "Autocomplete", "Organisation", false, new { htmlAttributes = new { #class = "form-control" } })
can anyone help ?
The key is to use the linq "Take"
This works: 1) Create an action in your controller and set the RouteConfig to start this action
public class HomeController : Controller
{
public ActionResult Index20()
{
MyViewModel m = new MyViewModel();
return View(m);
}
Create a view without any type of master page
Add this view model:
public class MyViewModel
{
public string SourceCaseNumber { get; set; }
}
Go to Manage Nuget Packages or PM Console and add to MVC 5 project - Typeahead.js for MVC 5 Models by Tim Wilson
Change the namespace for the added HtmlHelpers.cs to System.Web.Mvc.Html and rebuild
Add this class:
public class CasesNorm
{
public string SCN { get; set; }
}
Add these methods to your controller:
private List<Autocomplete> _AutocompleteSourceCaseNumber(string query)
{
List<Autocomplete> sourceCaseNumbers = new List<Autocomplete>();
try
{
//You will goto your Database for CasesNorm, but if will doit shorthand here
//var results = db.CasesNorms.Where(p => p.SourceCaseNumber.Contains(query)).
// GroupBy(item => new { SCN = item.SourceCaseNumber }).
// Select(group => new { SCN = group.Key.SCN }).
// OrderBy(item => item.SCN).
// Take(10).ToList(); //take 10 is important
CasesNorm c1 = new CasesNorm { SCN = "11111111"};
CasesNorm c2 = new CasesNorm { SCN = "22222222"};
IList<CasesNorm> aList = new List<CasesNorm>();
aList.Add(c1);
aList.Add(c2);
var results = aList;
foreach (var r in results)
{
// create objects
Autocomplete sourceCaseNumber = new Autocomplete();
sourceCaseNumber.Name = string.Format("{0}", r.SCN);
sourceCaseNumber.Id = Int32.Parse(r.SCN);
sourceCaseNumbers.Add(sourceCaseNumber);
}
}
catch (EntityCommandExecutionException eceex)
{
if (eceex.InnerException != null)
{
throw eceex.InnerException;
}
throw;
}
catch
{
throw;
}
return sourceCaseNumbers;
}
public ActionResult AutocompleteSourceCaseNumber(string query)
{
return Json(_AutocompleteSourceCaseNumber(query), JsonRequestBehavior.AllowGet);
}
throw;
}
catch
{
throw;
}
return sourceCaseNumbers;
}
public ActionResult AutocompleteSourceCaseNumber(string query)
{
return Json(_AutocompleteSourceCaseNumber(query), JsonRequestBehavior.AllowGet);
}
credit goes to http://timdwilson.github.io/typeahead-mvc-model/
(I realize this question is very similar to How to whitelist/blacklist child object fields in the ModelBinder/UpdateModel method? but my situation is slightly different and there may be a better solution available now that wasn't then.)
Our company sells web-based software that is extremely configurable by the end-user. The nature of this flexibility means that we must do a number of things at run time that would normally be done at compile time.
There are some rather complex rules regarding who has read or read/write access to most everything.
For instance, take this model that we would like to create:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace j6.Business.Site.Models
{
public class ModelBindModel
{
[Required]
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string FirstName { get; set; }
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string MiddleName { get; set; }
[Required]
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string LastName { get; set; }
[Required]
[Whitelist(ReadAccess = User.CanReadSalary, WriteAccess = User.CanWriteSalary)]
public string Salary { get; set; }
[Required]
[Whitelist(ReadAccess = User.CanReadSsn, WriteAccess = User.CanWriteSsn)]
public string Ssn { get; set; }
[Required]
public string SirNotAppearingOnThisPage { get; set; }
}
}
In the controller, it is not difficult to "unbind" things manually.
var resetValue = null;
modelState.Remove(field);
pi = model.GetType().GetProperty(field);
if (pi == null)
{
throw new Exception("An exception occured in ModelHelper.RemoveUnwanted. Field " +
field +
" does not exist in the model " + model.GetType().FullName);
}
// Set the default value.
pi.SetValue(model, resetValue, null);
Using HTML helpers, I can easily access the model metadata and suppress rendering of any fields the user does not have access to.
The kicker: I can't figure out how to access the model metadata anywhere in the CONTROLLER itself to prevent over-posting.
Note that using [Bind(Include...)] is not a functional solution, at least not without additional support. The properties to Include are run-time (not compile time) dependent, and excluding the property does not remove it from the validation.
ViewData.Model is null
ViewData.ModelMetaData is null
[AllowAnonymous]
[HttpPost]
// [Bind(Exclude = "Dummy1" + ",Dummy2")]
public ViewResult Index(ModelBindModel dto)
{
zzz.ModelHelper.RemoveUnwanted(ModelState, dto, new string[] {"Salary", "Ssn"});
ViewBag.Method = "Post";
if (!ModelState.IsValid)
{
return View(dto);
}
return View(dto);
}
Any suggestions on how to access the Model MetaData from the controller? Or a better way to whitelist properties at run time?
Update:
I borrowed a page from this rather excellent resource:
http://www.dotnetcurry.com/ShowArticle.aspx?ID=687
With a model that looks like this:
[Required]
[WhiteList(ReadAccessRule = "Nope", WriteAccessRule = "Nope")]
public string FirstName { get; set; }
[Required]
[WhiteList(ReadAccessRule = "Database.CanRead.Key", WriteAccessRule = "Database.CanWrite.Key")]
public string LastName { get; set; }
The class:
public class WhiteList : Attribute
{
public string ReadAccessRule { get; set; }
public string WriteAccessRule { get; set; }
public Dictionary<string, object> OptionalAttributes()
{
var options = new Dictionary<string, object>();
var canRead = false;
if (ReadAccessRule != "")
{
options.Add("readaccessrule", ReadAccessRule);
}
if (WriteAccessRule != "")
{
options.Add("writeaccessrule", WriteAccessRule);
}
if (ReadAccessRule == "Database.CanRead.Key")
{
canRead = true;
}
options.Add("canread", canRead);
options.Add("always", "be there");
return options;
}
}
And adding these lines to the MetadataProvider class mentioned in the link:
var whiteListValues = attributes.OfType<WhiteList>().FirstOrDefault();
if (whiteListValues != null)
{
metadata.AdditionalValues.Add("WhiteList", whiteListValues.OptionalAttributes());
}
Finally, the heart of the system:
public static void DemandFieldAuthorization<T>(ModelStateDictionary modelState, T model)
{
var metaData = ModelMetadataProviders
.Current
.GetMetadataForType(null, model.GetType());
var props = model.GetType().GetProperties();
foreach (var p in metaData.Properties)
{
if (p.AdditionalValues.ContainsKey("WhiteList"))
{
var whiteListDictionary = (Dictionary<string, object>) p.AdditionalValues["WhiteList"];
var key = "canread";
if (whiteListDictionary.ContainsKey(key))
{
var value = (bool) whiteListDictionary[key];
if (!value)
{
RemoveUnwanted(modelState, model, p.PropertyName);
}
}
}
}
}
To recap my interpretation of your question:
Field access is dynamic; some users may be able to write to a field and some may not.
You have a solution to control this in the view.
You want to prevent a malicious form submission from sending restricted properties, which the model binder will then assign to your model.
Perhaps something like this?
// control general access to the method with attributes
[HttpPost, SomeOtherAttributes]
public ViewResult Edit( Foo model ){
// presumably, you must know the user to apply permissions?
DemandFieldAuthorization( model, user );
// if the prior call didn't throw, continue as usual
if (!ModelState.IsValid){
return View(dto);
}
return View(dto);
}
private void DemandFieldAuthorization<T>( T model, User user ){
// read the model's property metadata
// check the user's permissions
// check the actual POST message
// throw if unauthorized
}
I wrote an extension method a year or so ago that has stood me in good stead a couple of times since. I hope this is of some help, despite not being perhaps the full solution for you. It essentially only allows validation on the fields that have been present on the form sent to the controller:
internal static void ValidateOnlyIncomingFields(this ModelStateDictionary modelStateDictionary, FormCollection formCollection)
{
IEnumerable<string> keysWithNoIncomingValue = null;
IValueProvider valueProvider = null;
try
{
// Transform into a value provider for linq/iteration.
valueProvider = formCollection.ToValueProvider();
// Get all validation keys from the model that haven't just been on screen...
keysWithNoIncomingValue = modelStateDictionary.Keys.Where(keyString => !valueProvider.ContainsPrefix(keyString));
// ...and clear them.
foreach (string errorKey in keysWithNoIncomingValue)
modelStateDictionary[errorKey].Errors.Clear();
}
catch (Exception exception)
{
Functions.LogError(exception);
}
}
Usage:
ModelState.ValidateOnlyIncomingFields(formCollection);
And you'll need a FormCollection parameter on your ActionResult declaration, of course:
public ActionResult MyAction (FormCollection formCollection) {
I am trying to pass this from my controller into my view (#ViewBag.Chapter7Total):
ViewBag.Chapter7Total = calc.CalculatePrice(quoteData, Chapter7);
But am getting a "doesn't exist in the current context error" in VS.
Basically, I am trying to pass in a second parameter which determines which pricing structure to use between 2. Chapter7 or Chapter13, with the selection determining the second parameter to perform calculations with.
Here are my methods:
class Chapter
{
public decimal PaymentPlan { get; set; }
public decimal Price { get; set; }
}
public decimal decPaymentPlan(QuoteData quoteData, Chapter chapter)
{
if (quoteData.StepFilingInformation.PaymentPlanRadioButton
== StepFilingInformation.PaymentPlan.No)
return PriceQuote.priceNoPaymentPlan;
else
return chapter.PaymentPlan;
}
public decimal Calculate(QuoteData quoteData, Chapter chapter)
{
decimal total = chapter.Price;
total += this.decPaymentPlan(quoteData, chapter);
return total;
}
static Chapter Chapter7 = new Chapter() { Price = 799.00m, PaymentPlan = 100.00m };
Finally, this is my controller:
public ActionResult EMailQuote()
{
Calculations calc = new Calculations();
Chapter chap = new Chapter();
QuoteData quoteData = new QuoteData
{
StepFilingInformation = new Models.StepFilingInformation
{
//just moking user input here temporarily to test out the UI
PaymentPlanRadioButton = Models.StepFilingInformation.PaymentPlan.Yes,
}
};
var total = calc.CalculatePrice(quoteData);
ViewBag.Chapter7Total = calc.CalculatePrice(quoteData, Chapter7);
return View(quoteData);
}
I'm not sure what to do to pass Chapter7. Any thoughts?
UPDATE 1:
This is my ViewModel (QuoteData):
public class QuoteData
{
public PriceQuote priceQuote;
public Calculations calculations;
public StepFilingInformation stepFilingInformation { get; set; }
public QuoteData()
{
PriceQuote = new PriceQuote();
Calculations = new Calculations();
}
}
I'm trying to figure out what you are doing here but I see that most importantly, you are sending quoteData to your View. I'm making a guess here but I figure QuoteData is a custom entity type of yours and not a ViewModel.
To start, I would create a QuoteDataViewModel in your models with all the properties of QuoteData that you need, including
public class QuoteDataViewModel {
... all of your quoteData properties here
public Chapter Chapter7 { get; set; }
}
In your EMailQuote action, something similar to this
public ActionResult EMailQuote() {
...
var model = new QuoteDataViewModel();
var quoteData = new QuoteData();
... // map your quoteData to your model with Automapper or manually like
... // model.SomeProperty = quoteData.SomeProperty;
... // repeat for all properties
model.Chapter7 = Chapter7;
return View(model);
}
If you are posting this data back you would need your Post action to accept the new QuoteDataViewModel
public ActionResult EmailQuote(QuoteDataViewModel model) {
if(ModelState.IsValid) {
....//save data that was entered?
}
return View(model);
}
Your view would then take a QuoteDateViewModel
#model QuoteDataViewModel
This is all just how I would do it personally, I don't quite understand what you have going on, for example, this line:
var total = calc.CalculatePrice(quoteData);
I don't see total ever being used after you create it.
Anyway, that's just a sample of how I'd do it, create a model specific to the view, include any and all properties I need, populate the model in the controller and send it to the view
Update
Based on the OP comment that quoteData is a ViewModel, then just as above, adding the new property to hold the extra data is simple, by adding ...
public decimal QuoteTotal { get; set; }
public Chapter Chapter7 { get; set; }
...to the ViewModel
the controller populates
var total = calc.CalculatePrice(quoteData);
model.QuoteTotal = total;
model.Chapter7 = new Chapter();
model.Chapter7 = Chapter7;
In the View the values can be accessed like:
#Html.DisplayFor(model => model.QuoteTotal)
#Html.DisplayFor(model => model.Chapter7.PaymentPlan)
#Html.DisplayFor(model => model.Chapter7.Price)