I am using cache on SelectListItems in my asp.net MVC app to store SelectListItems I'm using on lots of pages. The issue is that when I'm using it trough DropDownListFor, if I provide a selected value to this DropDownListFor, the SelectListItems looks to be changed... And I want to keep th SelectListItems in cache without "selected" property!
Here is the cache:
public IEnumerable<SelectListItem> GetAllPersonnelCached()
{
CacheHelper.SaveToCache("mykey", valueToCache, 240); //HttpContext.Current.Cache.Add
CacheHelper.GetFromCache<IEnumerable<SelectListItem>>("mykey"); //HttpContext.Current.Cache["mykey"]
}
This is the properties of the model I'm using
public int? personnelSelected{ get; set; }
public IEnumerable<SelectListItem> personnelList{ get; set; }
And How I fill it:
responsibleSelected = 100;
personnelList = GetAllPersonnelCached();
And here is how I am using the data on HTML part
#Html.DropDownListFor(x => x.personnelSelected, Model.personnelList, "PlaceHolder", new { data_placeholder = " " })
When I am running this code for a webpage, it works well. BUT then, when I call the GetAllPersonnelCached, it gives me all item as expected but the ListItem that has id 100 is "selecteditem". WHY? The fact that I'm using DropDownListFor makes changes on the List (referenced by the cache property in memory)? If yes, how to prevent this? Make the select list item readonly?
Thanks to all
The source code of the DropDownListFor shows that this extension method internally sets the Selected property.
Because a SelectListItem is a reference type, this change occurs on the corresponding item in the cache.
One way to prevent this is to return new SelectListItem objects from the GetAllPersonnelCached method, instead of the original cached ones.
public IEnumerable<SelectListItem> GetAllPersonnelCached()
{
CacheHelper.SaveToCache("mykey", valueToCache, 240);
var cachedItems = CacheHelper.GetFromCache<IEnumerable<SelectListItem>>("mykey");
return cachedItems.Select(o => new SelectListItem(o.Text, o.Value);
}
You might consider not to cache the SelectListItem instances, but the personnel-data objects instead which you transform to SelectListItem instances on retrieval.
// Assume your PersonnelData looks like below.
class PersonnelData
{
int Id { get; set; }
string Name { get; set; }
}
public IEnumerable<SelectListItem> GetAllPersonnelCached()
{
// valueToCache is a list of PersonnelData objects.
CacheHelper.SaveToCache("mykey", valueToCache, 240);
var cachedPersonnelData = CacheHelper.GetFromCache<IEnumerable<PersonnelData>>("mykey");
return cachedPersonnelData.Select(o => new SelectListItem(o.Name, o.Id.ToString());
}
Related
I have this dropdown that should contain a list of items i.e.
Debug
Information
Warning
Danger
This is how I obtain the list
public IEnumerable<string> GetLogLevels()
{
var data = dbContext.EventLogs.Distinct().ToList();
var modifiedData = data.Select(u => u.Level);
return modifiedData;
}
This is my controller and viewmodel
public IActionResult Index()
{
var levels = new SelectList(logsData.GetLogLevels(),"Level","Level");
var llvm = new LevelsListViewModel
{
Levels = levels
};
return View(llvm);
}
public class LevelsListViewModel
{
public SelectList Levels { get; set; }
}
This is how I declare it in my view
var level = "<div class='col-sm-3 search-spacing'><label for='Level'>Level</label><select asp-for='Level' asp-items='#Model.Levels'></select></div>";
The problem now is that for some reason it loads an empty list. When I debug the controller, I can see my values.
On a sidenote, is that the correct way of obtaining one field and populating the list? I want a distinct value only.
The problem lies in this line:
var levels = new SelectList(logsData.GetLogLevels(),"Level","Level");
The GetLogLevels method returns a collection of string, which does not contain a Level property.
There's two ways of resolving this:
change the return type of the GetLogLevels method so it returns a collection of event logs which have a Level property; or
use a different contructor of the SelectList class which only takes a collection of objects without specifying the value property name nor the text property name, like var levels = new SelectList(logsData.GetLogLevels());
This line causing problem because GetLogLevels() method returns IEnumerable<string>, which doesn't have property named Level in its context:
var levels = new SelectList(logsData.GetLogLevels(),"Level","Level");
There are some solutions to solve this issue:
1) Converting directly to IEnumerable<SelectListItem>
// Controller action
var levels = logsData.GetLogLevels().Select(x => new SelectListItem { Text = x, Value = x }).ToList();
// Model class
public class LevelsListViewModel
{
public List<SelectListItem> Levels { get; set; }
}
2) Using SelectList overload with single parameter
var levels = new SelectList(logsData.GetLogLevels());
3) Using ToDictionary()
var levels = new SelectList(logsData.GetLogLevels().ToDictionary(), "Key", "Value");
Note: If EventLogs contains multiple columns, you can't use Distinct() on that way. You should use GroupBy and Select first distinct values:
var data = dbContext.EventLogs.GroupBy(x => x.Level).Select(gr => gr.FirstOrDefault()).ToList();
return data;
See below for updated summary...
I understand that using the 'For' Html Helpers is preferred but I'm having a problem with DropDownListFor when using it as a multi-select.
This example (DropDownList) works perfectly:
#Html.DropDownList(
"ProtocolDisciplines",
new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines.Select(pd => pd.DisciplineId)),
new { #class = "form-control", multiple = "multiple", size = "8" }
)
This example (DropDownListFor) works perfectly EXCEPT the default value(s) does not get set:
#Html.DropDownListFor(
model => model.ProtocolDisciplines,
new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines.Select(pd => pd.DisciplineId)),
new { #class = "form-control", multiple = "multiple", size = "8" }
)
UPDATES
Based on what I'm learning I've updated from the original post. Here is the code that is still not working. To be clear, it is doing everything perfectly EXCEPT it is not selecting the default value when rendered. In the example I'm working with there is only a single default value.
#Html.ListBoxFor(
model => model.ProtocolDisciplines,
new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines),
new { #class = "form-control", size = "8" }
)
I've have made certain that Disciplines (the list of all 16 Disciplines in the db) and ProtocolDisciplines (the list of Disciplines that belong to the Protocol) are the same type (DisciplineViewModel). Further, that class (see below) contains only 2 properties (DisciplineId and Discipline).
I have a breakpoint where the model is returns to the view and I have verified that both Disciplines and ProtocolDisciplines have the values expected so I am currently focusing on the view and the ListBoxFor helper. As a note, I have also tried the exact same code with a DropDownListFor helper with identical behavior).
I suspect the problem is in the creation of the MultiSelectList. As you can see, I'm using the overload (IEnumerable ListItems, string DataValue, string DataText, IEnumerable SelectedValues). It would seem that the SelectedValues are simply not getting a match on anything in the ListValues but I can't figure out why. The types used in the two are the same, the DataValue and DataTypes names match the members of the types (just to be safe). I know the ListItems is correct because the list renders them correctly.
I'm at a loss.
Reference:
public partial class DisciplineViewModel
{
public Guid DisciplineId { get; set; }
public string Discipline { get; set; }
}
Here is the model:
public partial class ProtocolViewModelEdit
{
[Key]
public Guid ProtocolId { get; set; }
[Display(Name = "Name")]
public string Protocol { get; set; }
public string ProtocolType { get; set; }
[Display(Name = "Type")]
public Guid ProtocolTypeId { get; set; }
[Display(Name = "Status")]
public Guid ProtocolStatusId { get; set; }
public virtual ICollection<ProtocolTypeViewModel> ProtocolTypes { get; set; }
public virtual ICollection<ProtocolStatusViewModel> ProtocolStatuses { get; set; }
public virtual ICollection<DisciplineViewModel> ProtocolDisciplines { get; set; }
public virtual ICollection<ProtocolXProgramViewModel> ProtocolPrograms { get; set; }
public virtual ICollection<DisciplineViewModel> Disciplines { get; set; }
public virtual ICollection<ProgramViewModel> Programs { get; set; }
}
You referred to a post on the MSDN forums wherein the OP describes the following:
1) The selectedValues parameter must be populated with a collection of key values only. It cannot be a collection of the selected objects, as the HtmlHelper does not apply the dataValueField to this collection for you.
2) If using the ListBox you cannot set the name parameter the same as a Model property. Also you cannot name the ViewBag property that will contain the collection of items the same as the Model property.
3) If using the ListBoxFor it gets even more wonky. You should name the ViewBag property that will contain the collection of items the same as the Model property name. Now when you use the ListBoxFor within the View you must use a ViewBag property that does not exist (this is important!). The HtmlHelper.ListBoxFor will look automatically for a ViewBag property with the assigned Model property name.
None of these are actual issues. A SelectList ultimately has to be translated to/from an HTML select element, which can only work with simple types (string, int, etc.). Actually, everything is a string, and it's only the work of the model binder that translates the posted values into more specific types like int. As a result, it's obvious why you cannot bind a list of objects.
The other two mentioned issues are a result of ModelState. The values of bound form fields are determined by what's in ModelState, which is composed of values from Request, ViewData/ViewBag, and finally Model, as a last resort. If you set a SelectList in ViewBag with the same name as a property on Model, then the value for that key in ModelState will be that SelectList rather than the actual selected values, and your select will therefore have no selected items, because none of the option values will of course match that SelectList instance. Again, this is just standard behavior, and it's only a "bug" if you're not aware of how things work, and don't realize the implications of what you're doing.
Your issue here is exactly the first problem. You're passing a list of objects as the selected values, and there's simply no way to bind that properly to an HTML select element. However, things are far easier if you don't even bother to create your own MultiSelectList anyways. All the helper needs is IEnumerable<SelectListItem>. Razor will take care of creating a SelectList/MultiSelectList and setting the appropriate selected values. Just do:
#Html.ListBoxFor(
m => m.ProtocolDisciplines,
Model.Disciplines.Select(d => new SelectListItem { Value = d.DisciplineId.ToString(), Text = d.Discipline }),
new { #class = "form-control", size = 8 }
)
UPDATE
To answer you question about how Razor "knows", like I said in my answer, the info comes from ModelState. However, as pointed out by Stephen in the comments below, the property you're binding this to is a collection of objects. That's never going to work. Again, the posted values from an HTML select element will always be simple types, not objects. As a result, you need a property that the model binder can bind the posted data to, and then you need to use that information to lookup the actual objects you need, before finally setting something like your ProtocolDisciplines property. In other words:
public List<int> SelectedProtocolDisciplines { get; set; }
public IEnumerable<SelectListItem> DisciplineOptions { get; set; }
Since you're using a view model, it's better if you include the select list items on that view model, so I added a property for that. In your actions (GET and POST), you'll need to set this property:
model.DisciplineOptions = model.Disciplines..Select(d => new SelectListItem {
Value = d.DisciplineId.ToString(),
Text = d.Discipline
});
Since you'll need to call that in both the GET and POST actions, you might want to factor it out into a private method on your controller that both can call. Then, in your view:
#Html.ListBoxFor(m => m.SelectedProtocolDisciplines, Model.DisciplineOptions, new { #class = "form-control" })
Finally, in your POST action:
var protocolDisciplines = db.Disciplines.Where(m => model.SelectedProtocolDisciplines.Contains(m.DisciplineId));
Then, if this is a "create" method, you can simply set the appropriate property on your entity with that. If you're editing an existing entity, you'll need to do a little bit more work:
// Remove deselected disciplines
entity.ProtocolDisciplines
.Where(m => !model.SelectedProtocolDisciplines.Contains(m.DisciplineId))
.ToList()
.ForEach(m => entity.ProtocolDisciplines.Remove(m));
// Add new selected disciplines
var addedDisciplineIds = model.SelectedProtocolDisciplines.Except(entity.ProtocolDisciplines.Select(m => m.DisciplineId));
db.Disciplines
.Where(m => addedDisciplineIds.Contains(m.DisciplineId))
.ToList()
.ForEach(m => entity.ProtocolDisciplines.Add(m));
This extra footwork is necessary to maintain the existing, unchanged M2M relationships.
I am attempting to rid my code of the dependency on System.Web.Mvc, since I have extracted my model out to a separate .dll (outside of the MVC project) so that it can be shared among many projects.
I am attempting to create a "custom SelectListItem" which is simply just a class with two properties of string Text and string Value and looks like:
public class DropDownListItem
{
public string Text { get; set; }
public string Value { get; set; }
}
However, I am trying to find a way to pass this object into a DropDownListFor in my view. Is there anyway to manipulate this so that I can do something like:
#Html.DropDownListFor(x => x.Value, Model.Values, new { #class="form-control" })
Where Models.Values is of type List<DropDownListItem>.
Yo don't need to create a DropDownListItem class - instead you can create your own POCO (Plain-Old-C#-Object) to contain List Options:
For example, let say you have the following class :
class MyClass
{
public int Id {get; set; }
public string name {get; set; }
}
to have dropDownlist which contains a list of MyClass, you can simply use :
#Html.DropDownListFor(model => model.Entity.Id, new SelectList(MyClass, "Id", "Name"))
The id represent the value of the selection, and the name will be the text displayed within the DropDownList.
Hope this helps!
You can put the list item in viewbag and get the value in your view like these:
#Html.DropDownList("Ligas")
and your codebehind
Modelo.AdminSitio = Modelo.GetAdminSitio();
ViewBag.Ligas = new SelectList(Modelo.AdminSitio.ListadoLigas, "Value", "Text");
when listdoLigas is equals
ListadoLigas= (from x in db.Ligas select new DropDownListItem { Value = x.Oid_Liga, Text = x.NombreDeLiga}).ToList();
I am trying to build a drop down list using enum.
I tried the following, but do not know how to display it in view. I am using MVC framework
public enum Condition
{
And,
Or,
Not,
}
private List<Condition> userTypes = Enum.GetValues(typeof(Condition)).Cast<Condition>().ToList();
public List<Condition> UserType
{
get
{
return userTypes;
}
set
{
userTypes = value;
}
}
Is the above code right to display a simple drop down list?
And how do I pass in it view to display drop down list.
Thanks
in your Action :
ViewData["ddl"] = userTypes.Select(t => new SelectListItem { Text = t.ToString(), Value = ((int)t).ToString() });
in your aspx :
<%=Html.DropDownList("ddl", ViewData["ddl"] as IEnumerable<SelectListItem>)%>
Rest of this is alright.
You suppose to return string list from property UserType not Condition type. Secondly property must is of readonly since enum is constant and user won`t going to change it. Lastly don't use variable, property itself handle this.
public List<string> UserType
{
get
{
return Enum.GetNames(typeof(Condition)).ToList();
}
}
In your model add a List like:
private List conditionList= Enum.GetValues(typeof(Condition))
.Cast()
.Select(e => new SelectListItem { Value = ((int)e).ToString(), Text = e.ToString() });
And Then just add this on your view
#Html.EditorFor(m=>m.Condition,Model.conditionList)
I believe that will make things more easier.
When I am changing the "model => model.id" to "model => model.Supplierid" i am getting below error
"The parameter 'expression' must evaluate to an IEnumerable when
multiple selection is allowed."
please have look on below code
// this my model class
public class clslistbox{
public int id { get; set; }
public int Supplierid { get; set; }
public List<SuppDocuments> lstDocImgs { get; set; }
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
public List<SuppDocuments> listDocImages()
{
List<SuppDocuments> _lst = new List<SuppDocuments>();
SuppDocuments _supp = new SuppDocuments();
_supp.Title = "title";
_supp.documentid = 1;
_lst.Add(_supp);
return _lst;
}
}
// this my controller
[HttpGet]
public ActionResult AddEditSupplier(int id)
{
clslistbox _lst = new clslistbox();
_lst.lstDocImgs= _lst.listDocImages();
return View(_lst);
}
// this is view where i am binding listboxfor
#model clslistbox
#using (Html.BeginForm("AddEditSupplier", "Admin", FormMethod.Post))
{
#Html.ListBoxFor(model => model.id, new SelectList(Model.lstDocImgs, "documentid", "title"))
}
Can anyone see the reason for it?
I think the changing of the property in the expression here is a red-herring - it won't work in either case.
Update
However, see at the end of my answer for some probably needlessly detailed exposition on why you didn't get an error first-time round.
End Update
You're using ListBoxFor - which is used to provide users with multiple selection capabilities - but you're trying to bind that to an int property - which cannot support multiple selection. (It needs to be an IEnumerable<T> at least to be able to bind a list box to it by default in MVC)
I think you mean to be using DropDownListFor - i.e. to display a list of items from which only one can be selected?
If you're actually looking for single-selection semantics in a listbox, that's trickier to do in MVC because it's Html helpers are geared entirely around listboxes being for multiple selection. Someone else on SO has asked a question about how to get a dropdown to look like a list box: How do I create a ListBox in ASP.NET MVC with single selection mode?.
Or you could generate the HTML for such a listbox yourself.
(Update) - Potentially needlessly detailed exposition(!)
The reason you don't get an exception first time round is probably because there was no value for id in ModelState when the HTML was generated. Here's the reflected MVC source (from SelectExtensions.SelectInternal) that's of interest (the GetSelectListWithDefaultValue call at the end is the source of your exception):
object obj =
allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string[])) :
htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string));
if (!flag && obj == null && !string.IsNullOrEmpty(name))
{
obj = htmlHelper.ViewData.Eval(name);
}
if (obj != null)
{
selectList =
SelectExtensions.GetSelectListWithDefaultValue(selectList, obj, allowMultiple);
}
Note first that the control variable allowMultiple is true in your case, because you've called ListBoxFor. selectList is the SelectList you create and pass as the second parameter. One of the things that MVC (unfortunately in some cases) does is to use ModelState to modify the select list you pass when re-displaying a view in order to ensure that values which were set in ModelState via a POST are re-selected when the view is reloaded (this is useful when page validation fails because you won't copy the values to your underlying model from ModelState, but the page should still show those values as being selected).
So as you can see on the first line, the model's current value for the expression/field you pass is fished out of model state; either as a string array or as a string. If that fails (returns null)then it makes another go to execute the expression (or similar) to grab the model value. If it gets a non-null value from there, it calls SelectExtensions.GetSelectListWithDefaultValue.
As I say - what you're trying to do will ultimately not work in either the case of Id or SupplierId (because they would need to be IEnumerable) but I believe this ModelState->Eval process is yielding a null value when you use Id, so the process of getting an 'adjusted' SelectList is skipped - so the exception doesn't get raised. The same is not true when you use SupplierId because I'll wager that there's either a value in ModelState at that point, or the ViewData.Eval successfully gets an integer value.
Not throwing an exception is not the same as working!.
End update
Try changing your property from int to int[]
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
Assuming above is the class used for binding the model , try changing the documentid property as below
public class SuppDocuments
{
public string Title { get; set; }
public int[] documentid { get; set; }
}