MVC Razor MultiSelectList Getting/Setting SelectedValue - c#

I'm using erichynds Multi Select Widget to create a nice style for my MultiSelectList. But my issue (appears) to be unrelated.
I am trying to loop through each DemographicQuestionFilter question, list out the DemographicResponseFilter response and be able to get/post the selected items along with my DemographicFilterViewModel model. The issue I am having is that when I set the filters for item-1 (index 0) in the list it works fine, when I set item-2 (index 1) it only works if item-1 is also set, if item-1 is not set then the DemographicFilters object is null. I'm assuming I can switch up types, or that I'm missing something basic here.
How can I make it so that the list containing the selected items for n Question is not dependent n-1 also having a selected item?
Here are my ViewModel objects:
Parent:
public class DemographicFilterViewModel
{
public int TaskID { get; set; }
public List<DemographicQuestionFilter> DemographicFilters { get; set; }
}
Child:
public class DemographicQuestionFilter
{
public string Question { get; set; }
public List<DemographicResponseFilter> Responses { get; set; }
public List<SelectListItem> selectListItems { get; set; }
public List<int> SelectedItems { get; set; }
}
Grandchild:
public class DemographicResponseFilter
{
public int ResponseID { get; set; }
public string Response { get; set; }
}
View:
#Html.HiddenFor(m => m.TaskID)
if (Model.DemographicFilters != null)
{
for (int i = 0; i < Model.DemographicFilters.Count; i++)
{
#Html.HiddenFor(model => model.DemographicFilters[i].SelectedItems)
#Html.DisplayTextFor(m => m.DemographicFilters[i].Question)
<br />
#Html.ListBoxFor(model => model.DemographicFilters[i].SelectedItems, new MultiSelectList(Model.DemographicFilters[i].Responses, "ResponseID", "Response", Model.DemographicFilters[i].SelectedItems), new { Multiple = "multiple" })
<br />
<br />
}
}
Here is what is rendered to the screen (just so you can try to follow what I am doing):
http://i.imgur.com/ZefpLy1.png?1
Edit: The issue is when the View posts back to the controller, the View displays correctly, but on HttpPost the values in [n]SelectedItems are dependent on [n-1]SelectedItems having a value,
If [i]SelectedItems is blank (nothing selected) then every [>i]SelectedItems is null, even when the values are correctly set in the HttpGet...
HTMLHelper Extension:
#region Usings
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc.Html;
using System.Web.Mvc;
#endregion
namespace Extensions
{
public static class HtmlHelperExtensions
{
public static MvcHtmlString HiddenEnumerableFor<TModel, TEnumType>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, IEnumerable<TEnumType>>> expression)
{
return htmlHelper.Hidden(htmlHelper.NameFor(expression).ToHtmlString(),
string.Join(",", expression.Compile().Invoke(htmlHelper.ViewData.Model) ?? new TEnumType[0]));
}
}
}

HiddenFor cannot be used for ListBoxFor so here is the workaround I tried to fix the issue.
Replaced
#Html.HiddenFor(model => filter.SelectedItems)
With
#Html.Hidden(string.Format("DemographicFilters[{0}].SelectedItems", i), "-1")
Problem with this approach is that your DemographicFilters.SelectedItems will have an extra row -1 added to it, need to add code to exclude -1 row.

My answer is an extension on what Vasanth Sundaralingam posted in his answer, that HiddenFor won't work with arrays. I went ahead and created a function that behaves like a hiddenFor for enumerable properties.
#functions
{
public MvcHtmlString HiddenEnumerableFor<TModel, TEnumType>(
HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, IEnumerable<TEnumType>>> expression)
{
return htmlHelper.Hidden(htmlHelper.NameFor(expression).ToHtmlString(),
string.Join(",", expression.Compile().Invoke(htmlHelper.ViewData.Model) ?? new TEnumType[0]));
}
}
Replace
#Html.HiddenFor(model => filter.SelectedItems)
With
#HiddenEnumerableFor(Html, m => m.DemographicFilters[i].SelectedItems)
You could also convert this into an extension method by adding it to a static class, and adding this before the first parameter. That way it would look like very similar to HiddenFor
#Html.HiddenEnumerableFor(m => m.DemographicFilters[i].SelectedItems)

Related

Creating a Reusable Enum to Checkbox List using Razor Pages Partial, TagHelper or ViewComponent

I am attempting to create a neat, reusable checkbox list for enums with the [flags] attribute in Razor Pages with .NET Core 7.
I do not know which worflow to use - Partial, TagHelpers, ViewComponent or any others (or a combination), neither how I might apply these tools (having no experience creating any of them) to create a clean, efficient and reusable tool/helper.
The code below works, but it is not particularly reusable - for example, if I wanted to change the html so the label element became a parent of the checkbox input, I will need to change this in every instance of the 'cut and pasted' cshtml code.
In addition, the call to the helper function MyHtmlHelpers.EnumToCheckboxList<Urgency>(nameof(TransportReferral), nameof(TransportReferral.Urgency), TransportReferral?.Urgency) seems verbose and inefficient when compared to TagHelpers. Instead, it would be ideal to be able to access all these arguments with a single reference - in a similar way the TagHelpers do with the asp-for attribute, but I do not know how this might be achieved.
public static partial class MyHtmlHelpers
{
public static IEnumerable<CheckboxListItem> EnumToCheckboxList<TEnum>(string? modelName, string propertyName, TEnum? enumValue) where TEnum : struct, Enum
{
string name = string.IsNullOrEmpty(modelName)
? propertyName
: modelName + '.' + propertyName;
string idPrefix = name.Replace('.', '_');
return Enum.GetValues<TEnum>().Select(e =>
{
var eStr = e.ToString();
var eInt = Convert.ToInt32(e).ToString();
// ignoring DisplayAttribute.Name
return new CheckboxListItem
{
Display = typeof(TEnum).GetMember(eStr)[0]
.GetCustomAttributes<DescriptionAttribute>(false)
.FirstOrDefault()?
.Description ?? SplitCamelCase(eStr),
IsChecked = enumValue.HasValue && enumValue.Value.HasFlag(e),
Value = eInt,
Name = name,
Id = idPrefix + '_' + eInt,
};
}).ToList();
}
public static string SplitCamelCase(string input)
{
return lowerUpper().Replace(input, "$1 $2");
}
[GeneratedRegex("([a-z])([A-Z])", RegexOptions.CultureInvariant)]
private static partial Regex lowerUpper();
}
public class CheckboxListItem
{
public string Display { get; set; }
public string Value { get; set; }
public string Name { get; set; }
public string Id { get; set; }
public bool IsChecked { get; set; }
}
consumed in a cshtml page like so:
#foreach (var e in MyHtmlHelpers.EnumToCheckboxList<Urgency>(nameof(TransportReferral), nameof(TransportReferral.Urgency), Model.TransportReferral?.Urgency))
{
<div class="form-check form-check-inline">
<input type="checkbox"
name="#e.Name"
id="#e.Id"
checked="#e.IsChecked"
value="#e.Value">
<label class="form-check-label" for="#e.Id">
#e.Display
</label>
</div>
}
So in summary, is there a way to refactor the above code, taking advantage of Razor pages tools, to make the cshtml markup more reusable and also allow the full name TransportReferral.Urgency and its value to be passed cleanly to the tool with a single argument, similarly to (or in the same way) the asp-for attribute does for taghelpers?
Essentially what you want to do is replicate the SelectTagHelper with enum support, except you want to render checkboxes instead of option elements. Given that, I would start with the source code for the SelectTagHelper
https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/SelectTagHelper.cs
and the GetEnumSelectList helper https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs#L398

.NET MVC Updating the View with a model property

I used the approach described in this article to create a drop down.
The Model
public class IceCreamFlavor
{
public int Id { get; set; }
public string Name { get; set; }
}
The View Model
public class ViewModel
{
private readonly List<IceCreamFlavor> _flavors;
[Display(Name = "Favorite Flavor")]
public int SelectedFlavorId { get; set; }
public IEnumerable<SelectListItem> FlavorItems
{
get { return new SelectList(_flavors, "Id", "Name");}
}
}
The View
#Html.LabelFor(m=>m.SelectedFlavorId)
#Html.DropDownListFor(m => m.SelectedFlavorId, Model.FlavorItems)
#Html.ValidationMessageFor(m=>m.SelectedFlavorId)
<input type="submit" value="Submit" />
This approach works fine.
Now I want to display a property of the Model on the same view. As an example assume we had the following properties.
public class IceCreamFlavor
{
public int Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
}
Now underneath the Dropdown I need to display the price as
Price : 15.99
How can I achieve this?
I would rather choose a another solution, since firing ajax for every selected input is useless and consuming.
Using the normal current DropDownListFor in addition with outputting the complete price list to hiding input value. value e.g: '10.0;12.0;...' which every value is than can be taken by simple JavaScript procedure with the option index as the mark for the value you should take.
Constructing a new MyDropDownListFor which will follow as the current but instead of just constructing normal <option>.. it will also add to that html tags the price or whatever additional parameter you want it to display as well. Examples: Here Here Here
No matter what solution you take, it will have to be combined with supporting simple JavaScript method which then renders the Selection and Displaying the Price which already been downloaded.
To render a property off of the model after submitting, you can just break into HTML to display it:
#if (Model.Price != 0.0F)
{
<b>Price #Model.Price.ToString("0.00") </b>
}
To achieve this, add a collection onto the ViewModel:
public class ViewModel
{
private readonly System.Collections.Generic.List<IceCreamFlavor> _flavors;
public ViewModel()
{
// Construct Flavors
}
public List<IceCreamFlavor> AllFlavors
{
get
{
return _flavors;
}
}
[Display(Name = "Favorite Flavor")]
public int SelectedFlavorId { get; set; }
public System.Web.Mvc.SelectList FlavorItems
{
get { return new System.Web.Mvc.SelectList(_flavors, "Id", "Name");}
}
}
Then on the View:
#if (Model.AllFlavors.Any(f => f.Id == Model.SelectedFlavorId))
{
<b>Price #Model.AllFlavors.First(f => f.Id == Model.SelectedFlavorId).Price.ToString("0.00") </b>
}
You could, of course, just expose the selected Flavor as a property on the ViewModel (similar display principle applies). But, the advantage of exposing all the Flavors as a property, is you can easily move to storing this in JavaScript on page and query that, rather than relying on the submit button.
Then you can roll your own drop down onchange events using JavaScript / JQuery to read from this object stored on page. (Or use AJAX to make a call to another action to return the value as needed..)
The solution not exposing all flavors is:
Property on ViewModel:
public IceCreamFlavor SelectedFlavor
{
get
{
return _flavors.FirstOrDefault(f => f.Id == this.SelectedFlavorId);
}
}
Display on View:
#if (Model.SelectedFlavor != null)
{
<b>Price #Model.SelectedFlavor.Price.ToString("0.00") </b>
}

How to ignore a property based on a runtime condition?

I have a simple pair of classes which for I've set up a mapping at initialization time.
public class Order {
public int ID { get; set; }
public string Foo { get; set; }
}
public class OrderDTO {
public int ID { get; set; }
public string Foo { get; set; }
}
...
Mapper.CreateMap<Order, OrderDTO>();
Now at a certain point I need to map an Order to an OrderDTO. BUT depending on some circumstances, I might need to ignore Foo during mapping. Let's also assume that I cannot "store" the condition in the source or destination object.
I know how I can configure the ignored properties at initialization time, but I have no idea how I could achieve such a dynamic runtime behavior.
Any help would be appreciated.
UPDATE
My use case for this behaviour is something like this. I have an ASP.NET MVC web grid view which displays a list of OrderDTOs. The users can edit the cell values individually. The grid view sends the edited data back to the server like a collection of OrderDTOs, BUT only the edited field values are set, the others are left as default. It also sends data about which fields are edited for each primary key. Now from this special scenario I need to map these "half-empty" objects to Orders, but of course, skip those properties which were not edited for each object.
The other way would be to do the manual mapping, or use Reflection somehow, but I was just thinking about if I could use AutoMapper in some way.
I've digged into the AutoMapper source code and samples, and found that there is a way to pass runtime parameters at mapping time.
A quick example setup and usage looks like this.
public class Order {
public int ID { get; set; }
public string Foo { get; set; }
}
public class OrderDTO {
public int ID { get; set; }
public string Foo { get; set; }
}
...
Mapper.CreateMap<Order, OrderDTO>()
.ForMember(e => e.Foo, o => o.Condition((ResolutionContext c) => !c.Options.Items.ContainsKey("IWantToSkipFoo")));
...
var target = new Order();
target.ID = 2;
target.Foo = "This should not change";
var source = new OrderDTO();
source.ID = 10;
source.Foo = "This won't be mapped";
Mapper.Map(source, target, opts => { opts.Items["IWantToSkipFoo"] = true; });
Assert.AreEqual(target.ID, 10);
Assert.AreEqual(target.Foo, "This should not change");
In fact this looks quite "technical", but I still think there are quite many use cases when this is really helpful. If this logic is generalized according to application needs, and wrapped into some extension methods for example, then it could be much cleaner.
Expanding on BlackjacketMack's comment for others:
In your MappingProfile, add a ForAllMaps(...) call to your constructor.
using AutoMapper;
using System.Collections.Generic;
using System.Linq;
public class MappingProfile : Profile
{
public MappingProfile()
{
ForAllMaps((typeMap, mappingExpression) =>
{
mappingExpression.ForAllMembers(memberOptions =>
{
memberOptions.Condition((o1, o2, o3, o4, resolutionContext) =>
{
var name = memberOptions.DestinationMember.Name;
if (resolutionContext.Items.TryGetValue(MemberExclusionKey, out object exclusions))
{
if (((IEnumerable<string>)exclusions).Contains(name))
{
return false;
}
}
return true;
});
});
});
}
public static string MemberExclusionKey { get; } = "exclude";
}
Then, for ease of use, add the following class to create an extension method for yourself.
public static class IMappingOperationOptionsExtensions
{
public static void ExcludeMembers(this AutoMapper.IMappingOperationOptions options, params string[] members)
{
options.Items[MappingProfile.MemberExclusionKey] = members;
}
}
Finally, tie it all together: var target = mapper.Map<Order>(source, opts => opts.ExcludeMembers("Foo"));

What is the use of the first parameter in HTML.DropDownListFor?

I'm reading the documentation on HTML.DropDownListFor and it states the following:
public static MvcHtmlString DropDownListFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList
)
What does the expression do? I've been reading and it says to bind it to a property?
I've used the following code:
#(Html.DropDownListFor(x => x.SiteList[0], new SelectList(Model.SiteList, "LocationID", "Description",Model.SiteList[5].LocationID)))
and that code works just as fine as:
#(Html.DropDownListFor(x => x.SiteList[0].LocationID, new SelectList(Model.SiteList, "LocationID", "Description",Model.SiteList[5].LocationID)))
and
#(Html.DropDownListFor(x => x.SiteList[0].Description, new SelectList(Model.SiteList, "LocationID", "Description",Model.SiteList[5].LocationID)))
where SiteList is a:
List<Site>
with Site being:
public class Site {
public string LocationID;
public string Description;
}
I don't understand what the purpose of the lambda is in the example and how it's being used in the output?
So you would normally have a separate property on your model for this next to your list. When you select a value from the list it sets the property that this is bound to.
class ListType{
public string Key { get; set; }
public string Value { get; set; }
}
class ViewModel{
public IEnumerable<SelectListItem> List { get; set; } //This is generated from a list of ListType objects
public string Selected { get; set; }
}
Then you can use:
#Html.DropDownListFor(x => x.Selected, Model.List)
This means when the form is posted back then the Selected property has the value of your selected item in the list.
See the html generated by the three expression. The name and id would be different in all the cases. These name is used while binding the selected value to the property of the model.The name of the element should be same as the name of the property to which the selected value is to be bidden upon posting of the page.The expression gives a friendly way to give name to the element to achieve the purpose.

How can I make Html.CheckBoxFor() work on a string field?

I'm using ASP.NET MVC3 with Razor and C#. I am making a form builder of sorts, so I have a model that has a collection of the following object:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public MyFormType Type { get; set; }
}
MyFormType is just an enum that tells me if the form field is a checkbox, or textbox, or file upload, or whatever. My editor template looks something like this (see the comment):
~/Views/EditorTemplates/MyFormField.cshtml
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.Value) // This does not work!
}
}
I tried casting/converting the m.Value to a bool in the lambda expression for CheckBoxFor(), but that threw an error. I would just manually construct a checkbox input, but CheckBoxFor() seems to do two things that I can't seem to replicate:
Creates a hidden input that somehow gets populated by the checkbox. This appears to be what the model binder picks up.
Generates the name form the object so that the model binder gets the value into the right property.
Does anyone know a way around using CheckBoxFor() on a string, or a way to replicate its functionality manually, so that I can make this work?
You could also add a property on your viewmodel:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public bool CheckBoxValue
{
get { return Boolean.Parse(Value); }
}
public MyFormType Type { get; set; }
}
Your view would be something like this:
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.CheckBoxValue) // This does work!
}
}
Use Boolean.TryParse if you want to avoid exceptions.
One way is to create your own htmlhelper extension method.
public static MvcHtmlString CheckBoxStringFor<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, string>> expression)
{
// get the name of the property
string[] propertyNameParts = expression.Body.ToString().Split('.');
string propertyName = propertyNameParts.Last();
// get the value of the property
Func<TModel, string> compiled = expression.Compile();
string booleanStr = compiled(html.ViewData.Model);
// convert it to a boolean
bool isChecked = false;
Boolean.TryParse(booleanStr, out isChecked);
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("id", propertyName);
checkbox.MergeAttribute("name", propertyName);
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("value", "true");
if (isChecked)
checkbox.MergeAttribute("checked", "checked");
TagBuilder hidden = new TagBuilder("input");
hidden.MergeAttribute("name", propertyName);
hidden.MergeAttribute("type", "hidden");
hidden.MergeAttribute("value", "false");
return MvcHtmlString.Create(checkbox.ToString(TagRenderMode.SelfClosing) + hidden.ToString(TagRenderMode.SelfClosing));
}
The usage is the same as CheckBoxFor helper (e.Value is a string)
#Html.CheckBoxStringFor(e => e.Value)
Use the Checkbox, this simple way works fine
#Html.CheckBox("IsActive", Model.MyString == "Y" ? true : false)
I had this problem as well but was unable to modify the view model. Tried mdm20s solution but as i suspected it does not work on collection properties (it does not add the indexes to the names and ids like the native html helpers). To overcome this you can use the Html.CheckBox instead. It adds the proper indexes and you can pass the value of the checkbox yourself.
If you really want to use an expression you can always write a wrapper similar to mdm20s but replace everything after the TryParse with
return Html.CheckBox("propertyName", isChecked). Obviously you will need to add using System.Web.Mvc.Html as well.

Categories

Resources