1

Relatively new to MVC and trying to get a cascading dropdown list working for train times.

After looking at a lot of posts, people say that you should stay away from ViewBag/ViewData and instead focus on ViewModels, but I just can't seem to get my head round it, and it's driving me up the wall. Any tutorial seems to be either to complex or too easy and the whole viewModel idea just hasn't clicked with me yet.

So here is my scenario: I have an admin system where staff can add individual train journeys. For each train time, I have an input form where the user can choose a Company, and from there, I'd like the dropdownlist underneath to populate with a list of journey numbers, which indicate routes. Once they have chosen a number, they can carry on with the rest of the form, which is quite large, including time of travel, facilities on the train etc.

I've created a viewmodel like so:

public class JourneyNumbersViewModel
    {
        private List<SelectListItem> _operators = new List<SelectListItem>();
        private List<SelectListItem> _journeys= new List<SelectListItem>();

        [Required(ErrorMessage = "Please select an operator")]
        public string SelectedOperator { get; set; }
        [Required(ErrorMessage = "Please select a journey")]
        public string SelectedJourney { get; set; }

        public List<SelectListItem> Journeys
        {
            get { return _journeys; }
        }
        public List<SelectListItem> Operators
        {
            get
            {
                foreach(Operator a in Planner.Repository.OperatorRepository.GetOperatorList())
                {
                    _operators.Add(new SelectListItem() { Text = a.OperatorName, Value = a.OperatorID.ToString() });
                }
                return _operators;
            }
        }
    }

In my controller, I have this for the Create view:

    public ActionResult Create()
    {
        return View(new JourneyNumbersViewModel());
    }

And this is where it isn't really working for me - if I change my model at the top of the Create view to: @model Planner.ViewModels.JourneyNumbersViewModel, then the rest of my form throws errors as the model is no longer correct for the rest of the form. Is this the way it is supposed to work - what if you need to reference multiple view models with a single view?

I know this is a simple thing and as soon as it clicks I'll wonder how on earth I could have struggled with it in the first place, but if anyone can point out where I'm going wrong, I'd be very grateful.

5
  • You should NOT stay away from ViewBag and ViewData! They were made to be used, and they are quite powerful for delivering extra data to the view without the need of a new strongly typed holder. Commented Nov 29, 2011 at 12:11
  • ViewData/Bag is the most useless, stupid, prone to errors concept from ASP MVC. I really don't understand why Microsoft decided to implement such a thing and I don't really known why everybody, including introductory tutorials keep insisting on it. Commented Nov 29, 2011 at 12:44
  • ViewData is, in fact, a view model that is implemented by default but without the goodies of the latter. To known what data is coming from the controller to the view you've to check all your source code and not simply a ViewModel class, in addition to not being strongly-typed and, because of that, prone to errors. You can't do validation, localization or use HelperFor properly. Bottom-line, just forget ViewData/Bag exists. Commented Nov 29, 2011 at 12:50
  • Yeah that's pretty much the range of arguments I've seen - for every person saying ViewBag, 2 are saying No ViewBag. I do really want to get the hang of ViewModels though, and if I can see it working in my application just once, I think that'll be enough. Anyone tried to do something similar to the above using ViewModels? I'm happy to clarify any part of the question if it's not clear... Commented Nov 29, 2011 at 13:23
  • In most cases the models you want to send to a view do not match the models from the database. Often times you will need to create a ViewModel to combine two or more models together for output. Other times you will want to use a ViewModel to markup validation rules for the views. The ViewModels may be similar to your models but the business logic may not match. You need to code your views to use your ViewModel and not your "Model". Your controllers send a ViewModel to the view, the view sends a ViewModel back to the controller on the POST and the controller maps to your View. Commented Nov 29, 2011 at 15:12

1 Answer 1

1

I have done something similar. Here is some of the code (apologies upfront for this being quite long, but I wanted to make sure you could re-create this on your side):

View looks like this:

using Cascading.Models
@model CascadingModel


@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Cascading Forms</h2>
<table>

@using(Html.BeginForm("Index", "Home"))
{
<tr>
    <td>@Html.LabelFor(m=>m.CategoryId)</td>
    <td>@Html.DropDownListFor(m => m.CategoryId, new SelectList(Model.Categories, "Id", "Name"), string.Empty)</td>
</tr>
<tr>
    <td>@Html.LabelFor(m=>m.ProductId)</td>
    <td>@Html.CascadingDropDownListFor(m => m.ProductId, new SelectList(Model.Products, "Id", "Name"), string.Empty, null, "CategoryId", "Home/CategorySelected")</td>
</tr>
<tr>
    <td>&nbsp;</td>
    <td><input type="submit" value="Go"/></td>
</tr>
}
</table>

the Model looks as follows:

public class CascadingModel
{
    public int CategoryId { get; set; }
    public List<Category> Categories { get; set; }
    public int ProductId { get; set; }
    public List<Product> Products { get; set; }
}

the real "clever" part of the system is the Html.CascadingDropDownListFor which looks as follows:

public static class MvcHtmlExtensions
{
    public static MvcHtmlString CascadingDropDownListFor<TModel, TProperty>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        IEnumerable<SelectListItem> selectList,
        string optionLabel,
        IDictionary<string, Object> htmlAttributes,
        string parentControlName,
        string childListUrl
        )
    {
        var memberName = GetMemberInfo(expression).Member.Name;

        MvcHtmlString returnHtml = Html.SelectExtensions.DropDownListFor(htmlHelper, expression, selectList, optionLabel, htmlAttributes);

        var returnString = MvcHtmlString.Create(returnHtml.ToString() + 
                    @"<script type=""text/javascript"">
                        $(document).ready(function () {
                            $(""#<<parentControlName>>"").change(function () { 
                                var postData = { <<parentControlName>>: $(""#<<parentControlName>>"").val() };
                                $.post('<<childListUrl>>', postData, function (data) {
                                    var options = """";
                                    $.each(data, function (index) {
                                        options += ""<option value='"" + data[index].Id + ""'>"" + data[index].Name + ""</option>"";
                                    });
                                    $(""#<<memberName>>"").html(options);
                                })
                                .error(function (jqXHR, textStatus, errorThrown) { alert(jqXHR.responseText); });
                            });
                        });
                     </script>"
                    .Replace("<<parentControlName>>", parentControlName)
                    .Replace("<<childListUrl>>", childListUrl)
                    .Replace("<<memberName>>", memberName));

        return returnString;

    }

    private static MemberExpression GetMemberInfo(Expression method)
    {
        LambdaExpression lambda = method as LambdaExpression;
        if (lambda == null)
            throw new ArgumentNullException("method");

        MemberExpression memberExpr = null;

        if (lambda.Body.NodeType == ExpressionType.Convert)
        {
            memberExpr = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
        }
        else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
        {
            memberExpr = lambda.Body as MemberExpression;
        }

        if (memberExpr == null)
            throw new ArgumentException("method");

        return memberExpr;
    }
}

Controller Logic for those looking for it:

public ActionResult CategoriesAndProducts()
{
    var viewModel = new CategoriesAndProductsViewModel();
    viewModel.Categories = FetchCategoriesFromDataBase();
    viewModel.Products = FetchProductsFromDataBase();
    viewModel.CategoryId = viewModel.Categories[0].CategoryId;
    viewModel.ProductId = viewModel.Products.Where(p => p.CategoryId).FirstOrDefault().ProductId;
    return View(viewModel);
}
Sign up to request clarification or add additional context in comments.

10 Comments

btw - thanks to many, many stackoverflow articles for helping me get to this final solution
Yes I concur - In the end for me I wasn't using ViewModels correctly. I did know that, but I was foolishly looking for a quick solution. It turned out I wasn't referring to all the other relevant classes in my viewmodel (it was quite a detailed view using a few foreign keys). Once I sorted out the viewmodels I was fine. I'll mark this as the correct answer as it was what I was looking for, but putting the time in to understand viewmodels and returning JSON was what solved it for me. Thanks very much
I've never had to make helpers before so I just made a new class and pasted the code in and added in as many Usings as I could think of but I still having issues with anything including Expression in it and (only this(Html)).SelectExtensions So MemberExp../LambdaExp.../etc all are missing their using. Do you still remember what using are needed?
using System.Linq.Expressions; solved the expression errors now I just need to resolve the html using issue
Er, this would have been a much more useful answer if the controller logic was included. -1
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.