3

The Problem: I generate an @Html.ListBoxFor based on a database query in my HttpGet. During my HttpPost, I want to validate that at least one element has been selected. If not, I just want to add a validation message.

The current result: I get the message "Please select at least one item" but now the Select is blank (The select element is there but contains 0 options). I understand that Model.Items will be null in my HttpPost.

Question: How can I use my model to persist Model.Items to make it not null?

Additional Information: I am trying to avoid using the FormCollection collection and additional JavaScript.

--The code--

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        MyViewModel model = new MyViewModel
        {
            Items = Enumerable.Range(1, 5).Select(x => new SelectListItem
            {
                Value = x.ToString(),
                Text = "item " + x
            })
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {

        return View(model);
    }
}

Model:

public class MyViewModel
{
    public MyViewModel()
    {
        Items = new List<SelectListItem>();
    }


    [Required(ErrorMessage = "Please select at least one item")]
    public string[] SelectedItems { get; set; }

    public IEnumerable<SelectListItem> Items { get; set; }
}

View:

@model ProjectGenerator.Models.MyViewModel

@using (Html.BeginForm())
{
    @Html.ListBoxFor(x => x.SelectedItems, Model.Items)
    @Html.ValidationMessageFor(x => x.SelectedItems)
    <button type="submit">OK</button>
}
Paul Totzke
  • 1,470
  • 17
  • 33
  • You need repopulate the `SelectList` in the POST method before you return the view (just as you did in the GET method). You don't (nor should you) generate inputs for each `SelectListItem` so they will not be in the form data. –  Sep 09 '15 at 22:37

3 Answers3

0
    public IEnumerable<SelectListItem> Items 
    {
        get
        {            
            if (HttpContext.Current.Session["MY_ITEMS_TO_LIST_FOR"] == null)
            {
                return null;
            }
            else
            {
                return (IEnumerable<SelectListItem>)HttpContext.Current.Session["MY_ITEMS_TO_LIST_FOR"];
            }
        }

        set
        {
            if (value.Count() > 0) //http post reset value
            {
                HttpContext.Current.Session["MY_ITEMS_TO_LIST_FOR"] = value;
            }
        }
    }

I tested this way and worked well. There are other ways, but I found this easier. If you debug the set of items Items property , you will see that for some reason the items are removed from the collection on http post, even if you use session. To prevent the collection in the session to receive an empty collection, I used If (Items.Count ()> 0). You can enlarge this idea and customize your get and set.

  • Would this work with concurrent tabs? For example, let's say the user has 2 tabs. On the first one, the select list has the values {1,2,3} and the second one has {2,3,4}. Then if the user posted-back on the one they opened first, the HttpContext.Current.Session["MY_ITEMS_TO_LIST_FOR"] would have been over written when the second page was loaded. – Paul Totzke Sep 10 '15 at 14:36
  • The values selected by the user is in SelectedItems variable that does not use Session. The property Items that uses session comes from the server to the client (one direction only - used to populate listbox) and every request will bring the updated value. If you use Session for the variable that will come from the client, you need to use a Session ID as explained in this post. http://stackoverflow.com/questions/14373456/how-to-distinguish-session-between-mutiple-tabs-of-browsers –  Sep 10 '15 at 15:17
  • If you use jquery, you could create a standard action in your controllers with json result and use the autocomplete in a TemplateView. In FIAT programs is used as well for all list box (suppliers, users, etc.). But how do you put that do not want javascript ... –  Sep 10 '15 at 15:24
  • I write an awnser using Json Result. –  Sep 10 '15 at 15:36
  • Ferreira, I wasn't aware SessionId was stored on page rather than a cookie. Thanks for that post. I was also was unaware that the form only posted the selected values rather than sending the whole form. The idea of storing them in the session still seems less than ideal (Sessiondata is lost when the server restarts). – Paul Totzke Sep 10 '15 at 16:42
  • Currently, I'm trying to serialize IEnumerable into a HiddenFor but no luck. If I can't find a solution that I like better, I'll mark this one was Most Helpful. – Paul Totzke Sep 10 '15 at 16:48
  • Consider using jquery as in the example posted. The collection is stored in cache if you want.It will be read in the database if you use the option not to store cache. https://jqueryui.com/autocomplete/ –  Sep 10 '15 at 17:16
  • I remembered one thing. You can also manipulate the client cache to persist your data. It's more complicated, but you can create a helper. http://stackoverflow.com/questions/343899/how-to-cache-data-in-a-mvc-application –  Sep 10 '15 at 17:20
0

You are not, and nor should you, create form controls for each property of each SelectListItem in property Items so they are not included in the form data when you submit. You need to reassign the SelectList in the POST method of you return the view

public ActionResult Index()
{
  MyViewModel model = new MyViewModel();
  ConfigureViewModel(model);
  return View(model);
}

[HttpPost]
public ActionResult Index(MyViewModel model)
{
  if (!ModelState.IsValid)
  {
    ConfigureViewModel(model);
    return View(model);
  }
  // Save and redirect
}

private void ConfigureViewModel(MyViewModel model)
{
  model.Items = Enumerable.Range(1, 5).Select(x => new SelectListItem
  {
    Value = x.ToString(),
    Text = "item " + x
  });
}
0

One Example using Json result and templateview. This is a good pattern.

Controller (you can configure json to not allow get):

   public class FornecedorController : BaseController
    {
        protected FornecedorServices Service = new FornecedorServices();

        [HttpGet]
        [Authorize(Roles = ApplicationRoles.FORNECEDOR_VISUALIZAR)]
        [OutputCache(NoStore = false, Duration = 3600)]
        public JsonResult ListarJson(FornecedorParameters parameters)
        {
            var model = this.Service.Search(parameters)
                .Select(x => new
                {
                    Value = x.Codigo,
                    Description = x.CodigoNomeFantasia
                });
            return this.Json(model, JsonRequestBehavior.AllowGet);
        }

    }

Template View (you can customize your own):

@model int[]
@{
    var id = "id" + Guid.NewGuid().ToString().Substring(0, 5);
    var disabled = (bool)(this.ViewData["disabled"] ?? false);
    var showAll = (bool)(this.ViewData["ShowAll"] ?? false);
    var state = this.ViewData.ModelState[Html.NameFor(x => x).ToString()];
    var size = (Size)(this.ViewData["Size"] ?? Size.Big);
    string css = (state != null && state.Errors.Count > 0) ? "input-validation-error" : string.Empty;

    List<SelectListItem> listValues;
    if (this.Model == null)
    {
        listValues = new List<SelectListItem>();
    }
    else
    {
        listValues = this.Model.Select(x => new SelectListItem { Selected = true, Value = x.ToString(), Text = x.ToString() }).ToList();
    }
}
<div class="field-@size @css">
    <h3>@Html.LabelFor(model => model):</h3>
        @Html.ListBox("", listValues, new { id = id })
</div>
<script language="javascript" type="text/javascript">
    $("#@id").turnAutoComplete("@Url.Action("ListarJson", "Fornecedor", new { ShowAll = showAll })"@if (showAll) { <text>, checkSelectAll</text> })
        .change(function () {
            @Html.Raw(this.ViewData["OnChange"])
        });
</script>