Custom DropDownListFor Helper loads an empty value for the selected element
Asked Answered
A

1

1

I am using a custom helper, to create a select element that can take HtmlAttribute. I am using the same code as I found here, in @Alexander Puchkov answer (which I'll post for reference).

It's working fine, apart when the DDL helper is loaded with one of the option as the selected item, then the value of the selected item is null/empty. (i.e. on an edit page the DDL loads up with the option that was set on creation, rather than the '-Please select option-'), The highlighted attribute in the picture shows the problem, the value should not be empty, but should show 'Medium'...

So the text is display correctly but the element has no value. Any ideas on where this issue comes from?

Here is the full code of the helper:

    /// <summary>
    /// A selectListItem with an extra property to hold HtmlAttributes
    /// <para>Used in conjunction with the sdDDL Helpers</para>
    /// </summary>
    public class SdSelectListItem : SelectListItem
    {
        public object HtmlAttributes { get; set; }
    }

    /// <summary>
    /// Generate DropDownLists with the possibility of styling the 'option' tags of the generated 'select' tag  
    /// </summary>
    public static class SdDdlHelper
    {
       public static MvcHtmlString sdDDLFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                Expression<Func<TModel, TProperty>> expression, IEnumerable<SdSelectListItem> selectList,
                                                                string optionLabel, object htmlAttributes)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);

            return SelectInternal(htmlHelper, metadata, optionLabel, ExpressionHelper.GetExpressionText(expression), selectList,
                false /* allowMultiple */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
        }

        public static MvcHtmlString sdDDLFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                Expression<Func<TModel, TProperty>> expression, IEnumerable<SdSelectListItem> selectList,
                                                                object htmlAttributes)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);

//-> The below line is my problem (specifically the 'null' param), it set to null if no option label is passed to the method...So if I use this overload, the DDL will load (or re-load) with the default value selected, not the value binded to the property - And if I use the above overload, and set Model.action_priority as the optionLabel, then I get what is shown in the picture...

            return SelectInternal(htmlHelper, metadata, null, ExpressionHelper.GetExpressionText(expression), selectList,
                false /* allowMultiple */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
        }


        #region internal/private methods

        private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name,
            IEnumerable<SdSelectListItem> selectList, bool allowMultiple,
            IDictionary<string, object> htmlAttributes)
        {
            string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
            if (String.IsNullOrEmpty(fullName))
                throw new ArgumentException("No name");

            if (selectList == null)
                throw new ArgumentException("No selectlist");

            object defaultValue = (allowMultiple)
                ? htmlHelper.GetModelStateValue(fullName, typeof(string[]))
                : htmlHelper.GetModelStateValue(fullName, typeof(string));

            // If we haven't already used ViewData to get the entire list of items then we need to
            // use the ViewData-supplied value before using the parameter-supplied value.
            if (defaultValue == null)
                defaultValue = htmlHelper.ViewData.Eval(fullName);

            if (defaultValue != null)
            {
                IEnumerable defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
                IEnumerable<string> values = from object value in defaultValues
                                             select Convert.ToString(value, CultureInfo.CurrentCulture);
                HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
                List<SdSelectListItem> newSelectList = new List<SdSelectListItem>();

                foreach (SdSelectListItem item in selectList)
                {
                    item.Selected = (item.Value != null)
                        ? selectedValues.Contains(item.Value)
                        : selectedValues.Contains(item.Text);
                    newSelectList.Add(item);
                }
                selectList = newSelectList;
            }

            // Convert each ListItem to an <option> tag
            StringBuilder listItemBuilder = new StringBuilder();

            // Make optionLabel the first item that gets rendered.
            if (optionLabel != null)
                listItemBuilder.Append(
                    ListItemToOption(new SdSelectListItem()
                    {
                        Text = optionLabel,
                        Value = String.Empty,
                        Selected = false
                    }));

            foreach (SdSelectListItem item in selectList)
            {
                listItemBuilder.Append(ListItemToOption(item));
            }

            TagBuilder tagBuilder = new TagBuilder("select")
            {
                InnerHtml = listItemBuilder.ToString()
            };
            tagBuilder.MergeAttributes(htmlAttributes);
            tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
            tagBuilder.GenerateId(fullName);
            if (allowMultiple)
                tagBuilder.MergeAttribute("multiple", "multiple");

            // If there are any errors for a named field, we add the css attribute.
            System.Web.Mvc.ModelState modelState;
            if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState))
            {
                if (modelState.Errors.Count > 0)
                {
                    tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
                }
            }

            tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(fullName, metadata));

            return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.Normal));
        }

        internal static string ListItemToOption(SdSelectListItem item)
        {
            TagBuilder builder = new TagBuilder("option")
            {
                InnerHtml = HttpUtility.HtmlEncode(item.Text)
            };
            if (item.Value != null)
            {
                builder.Attributes["value"] = item.Value;
            }
            if (item.Selected)
            {
                builder.Attributes["selected"] = "selected";
            }
            builder.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(item.HtmlAttributes));
            return builder.ToString(TagRenderMode.Normal);
        }

        internal static object GetModelStateValue(this HtmlHelper htmlHelper, string key, Type destinationType)
        {
            System.Web.Mvc.ModelState modelState;
            if (htmlHelper.ViewData.ModelState.TryGetValue(key, out modelState))
            {
                if (modelState.Value != null)
                {
                    return modelState.Value.ConvertTo(destinationType, null /* culture */);
                }
            }
            return null;
        }

        #endregion
    }

}

Here is how I call it in the View (using Razor):

 @Html.sdDDLFor(x => Model.action_priority, Model.actionPriorityDDL(), Model.action_priority, new
        {
            @id = "_Action_Priority_DDL",
            @class = "form-control"

        })

And finally here is the Model.actionPriorityDDL() method:

public List<SdSelectListItem> actionPriorityDDL()
        {
            action_priority_DDL = new List<SdSelectListItem>();

            action_priority_DDL.Add(new SdSelectListItem
            {
                Value = StringRepository.ActionPriority.high,
                Text = StringRepository.ActionPriority.high,
                HtmlAttributes = new
                {
                    @class = "lbl-action-priority-high"
                }
            }
            );

            action_priority_DDL.Add(new SdSelectListItem
            {
                Value = StringRepository.ActionPriority.medium,
                Text = StringRepository.ActionPriority.medium,
                HtmlAttributes = new
                {
                    @class = "lbl-action-priority-medium"

                }
            }
            );

            action_priority_DDL.Add(new SdSelectListItem
            {
                Value = StringRepository.ActionPriority.low,
                Text = StringRepository.ActionPriority.low,
                HtmlAttributes = new
                {
                    @class = "lbl-action-priority-low"

                }
            }
            );

            action_priority_DDL.Add(new SdSelectListItem
            {
                Value = StringRepository.ActionPriority.psar,
                Text = StringRepository.ActionPriority.psar,
                HtmlAttributes = new
                {
                    @class = "lbl-action-priority-psar"

                }
            }
           );
            return action_priority_DDL;
        }
Albuminate answered 3/8, 2017 at 8:32 Comment(8)
Why are you creating a <select> with 2 options that display "Medium"? And how does this vary from the inbuilt DropDownListFor() method?Myrlemyrlene
The first option "Medium" is the selected option, which somehow has no value (thisis the problem). The helper differs from the normal DropDownListFor() as it allows to add Html Attribute to the option elements of the <select>Albuminate
You need to study the source code. You using almost identical code but not understanding how it works. The 3rd parameter is the optionLabel which adds a default null option with no value (see the code following // Make optionLabel the first item that gets rendered.Myrlemyrlene
Change the usage to @Html.sdDDLFor(x => Model.action_priority, Model.actionPriorityDDL(), "Please Select", new{ ... }) so that the default option is <option value="">Please Select</option> (your currently adding 2 options with the same display text)Myrlemyrlene
I have two option that display the same text, the first one though, is the option that was selected by the user previously. This is for an edit page, so the form is already populated when it loads up. If I call the sdDDLFor() Helper as per your comment, the DDL shows the 'Please Select', and not the option that was selected by the user previously.Albuminate
Showing 2 options with the same text but different values makes no sense at all. And its the value of the property your binding to that determines what is selected.Myrlemyrlene
I agree, and I understand that the binding property should determine the value of the selected option, but when I use this custom helper it does not. And it append the selected option as an extra one, hence the 2 similar values. This does not happen when I use the built-in helper. That is why I think there is a bug in the code I am using...Albuminate
Compare your code with the source codeMyrlemyrlene
C
0

This is caused by a former bug in MVC framework (http://aspnet.codeplex.com/workitem/8311; accessible here: https://web.archive.org/web/20131208041521/http://aspnet.codeplex.com/workitem/8311) that happens when using the DropDownListFor helper within a loop, i.e. the model property has an indexer (like in your example, where the generated select element has an attribute name="actionList[0].action_priority")

I resolved this by copying some code from here https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/SelectExtensions.cs

Specifically, in this method

private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name,
    IEnumerable<ExtendedSelectListItem> selectList, bool allowMultiple,
    IDictionary<string, object> htmlAttributes)

replace this

    if (selectList == null)
        throw new ArgumentException("No selectlist");

with:

            bool usedViewData = false;

            // If we got a null selectList, try to use ViewData to get the list of items.
            if (selectList == null)
            {
                selectList = htmlHelper.GetSelectData(name);
                usedViewData = true;
}

Now, replace this

    if (defaultValue == null)
        defaultValue = htmlHelper.ViewData.Eval(fullName);

with:

            if (defaultValue == null && !String.IsNullOrEmpty(name))
            {
                if (!usedViewData)
                {
                    defaultValue = htmlHelper.ViewData.Eval(name);
                }
                else if (metadata != null)
                {
                    defaultValue = metadata.Model;
                }
}

Finally, you also need to add this method:

        private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
        {
            object o = null;
            if (htmlHelper.ViewData != null)
            {
                o = htmlHelper.ViewData.Eval(name);
            }
            if (o == null)
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        MvcResources.HtmlHelper_MissingSelectData,
                        name,
                        "IEnumerable<SelectListItem>"));
            }
            IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
            if (selectList == null)
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        MvcResources.HtmlHelper_WrongSelectDataType,
                        name,
                        o.GetType().FullName,
                        "IEnumerable<SelectListItem>"));
            }
            return selectList;
}

replacing SelectListItem with your custom class (e.g. SdSelectListItem, or ExtendedSelectListItem , or whatever you named it )

Cruller answered 19/4, 2018 at 6:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.