15

This maybe very simple but I cant seem to sort it out on my own. I have created a simple db and entity modal that looks like this

enter image description here

I am trying to create an Create form that allows me to add a new Order. I have a total of 3 tables so what I am trying to do is have the form allowing the person to enter Order date and also has a dropdown list that allows me to select a product from the product table

I want to be able to create a Add or Edit view that allow me to insert the OrderDate into the OrderTable and also insert the OrderID and selected ProductID into OrderProduct.

What steps do I need to do here.

I have created an OrderController and ticked the "Add Actions" and than added a Create View which looks like this

@model Test.OrderProduct

@{
    ViewBag.Title = "Create2";
}

    <h2>Create2</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>OrderProduct</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.OrderID)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.OrderID)
            @Html.ValidationMessageFor(model => model.OrderID)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.ProductID)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ProductID)
            @Html.ValidationMessageFor(model => model.ProductID)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

This creates the view that contains a textbox for both OrderID and ProductID however no date.

My controller CreatePost hasnt been changed

  [HttpPost]
    public ActionResult Create(FormCollection collection)
    {
        try
        {
            var data = collection;
            // TODO: Add insert logic here
          //  db.Orders.AddObject(collection);
            return RedirectToAction("Index");
        }
        catch
        {
            return View();
        }
    }

My questions are,

1.How do I swap out ProductID textbox to be a dropdown which is populated from Product 2.How do I get the data from FormCollection collection? I thought of just a foreach however I dont know how to get the strongly typed name

Any help for a newbie would be very helpful.

Thank you!

saggu
  • 73
  • 5
Diver Dan
  • 9,953
  • 22
  • 95
  • 166
  • It's funny that this is the exact question I have at the moment, and yet no one has answered it here. I'd have thought the MVC junkies would have jumped on this in 2 seconds flat. – Joel Etherton Feb 15 '11 at 12:23

3 Answers3

25

First thing's first, don't bind to the Order entity. Never bind to an EF object, always try and use a ViewModel. Makes life simpler for the View, and that is the goal here.

So, have a ViewModel like this:

public class CreateOrderViewModel
{
   public int OrderId { get; set; }
   public DateTime OrderDate { get; set; }
   public int SelectedProductId { get; set; }
   public IEnumerable<SelectListItem> Products { get; set; }
}

That's it right now.

Return that to your View in your [HttpGet] controller action:

[HttpGet]
public ActionResult Create()
{
   var model = new CreateOrderViewModel
   {
      Products = db.Products
                   .ToList() // this will fire a query, basically SELECT * FROM Products
                   .Select(x => new SelectListItem
                    {
                       Text = x.ProductName,
                       Value = x.ProductId
                    });
   };

   return View(model);
}

Then to render out the list of Products: (basic HTML excluded)

@model WebApplication.Models.CreateOrderViewModel

@Html.DropDownListFor(model => model.SelectedProductId, Model.Products)

The only thing i don't know how to do is bind to the DateTime field. I'm guessing you would need an extension method (HTML Helper) which renders out a Date Picker or something. For this View (creating a new order), just default to DateTime.Now.

Now, onto the [HttpPost] controller action:

[HttpPost]
public ActionResult Create(CreateOrderViewModel model)
{
   try
   {
      // TODO: this manual stitching should be replaced with AutoMapper
      var newOrder = new Order
      {
         OrderDate = DateTime.Now,
         OrderProduct = new OrderProduct
         {
            ProductId = SelectedProductId
         }
      };

      db.Orders.AddObject(newOrder);
      return RedirectToAction("Index");
   }
   catch
   {
      return View();
   }
}

Now, i also think your EF model needs work.

To me (in English terms), a Product can have many orders, and an Order can have many Products.

So, it should be a many-to-many. Currently it's a 1-1 with a redundant join table. Did you generate that from a DB? If so, your DB possibly needs work.

You should have a navigational property called Products on the Order entity, which references a collection of Product, made possible by a silent join to the join table in the many-to-many.

This also means you no longer have a DropDownList, but a MultiSelectDropDownList.

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • in the `[HttpGet]` method above, I keep getting an error telling me i cannot implicitly convert int to string for the `value = x.id` bit! I added a `ToString()` but then i get this error: Error 7 Cannot implicitly convert type 'System.Collections.Generic.IEnumerable' to 'System.Collections.Generic.IEnumerable'. An explicit conversion exists (are you missing a cast?) – Ben Mar 20 '12 at 12:09
  • @Ben - most likely your missing a using reference – RPM1984 Mar 20 '12 at 20:10
  • @RPM1984 Could you elaborate why not bind directly to EF? What is the reason? If one looks at it it looks simpler, annotations are respected and all. – Anderson Dec 14 '14 at 10:03
  • @Anderson - because you end up cluttering your view with meta data that isn't required. Let's say you have a table with 10 properties, some are timestamps, foreign keys, etc - the View doesn't care about this information. You should work out what the view needs, create a view model for that information only, then map it to/from the EF model IMO – RPM1984 Dec 14 '14 at 22:11
2

Thanks Craig. Your few days (as at time of posting) of MVC have solved my few days of trying to get the selected value back from DropDownListFor.

I had no problem in the Create view in getting the selected value of the DDLF, but the Edit view was a completely different matter - nothing I tried would get the selected value back in the Post. I noticed the selected value was lurking in the AttemptedValue of the ModelState, and so Dr.Google referred me here.

I had this in my view

    @Html.DropDownList(model => model.ContentKeyID, Model.ContentKeys, Model.ContentKeyName)

where ContentKeys is a SelectList populated from the DB via a ViewModel, and ContentKeyName is the curently selected name.

The wierd thing is, I have another DDL on the view populated in an identical manner. This one works. Why, I don't know. It is the second DDL on the form, but I can't see that making a difference.

I read somewhere else it might have been that I was using Guids as the Id, but that didn't seem to make a difference - I changed to Int32, but don't think I had to - I think it's enums that disagree with DDLF. Nullables seemd to make no difference either.

Now that I've added the form collection to my Post ActionResult, and get the selected value using

-view

    @Html.DropDownList("ContentKey", Model.ContentKeys)

-in controller (Post)

    contentKeyId = int.Parse(form.GetValue("ContentKey").AttemptedValue); 

all is good, and I can get on with more exciting things. Why is that the simplest things can hold you up for so long?

Rob
  • 21
  • 2
1

I have been struggling with this over the last day or so too. I'll share my limited knowledge in the hope that it will help someone else.

Note that I use a singleton with a pre-populated list to keep my example application small (i.e. no EF or DB interaction).

To show the ProductList you will need to have a ViewBag or ViewData item which contains the ProductList. You can set this up in the Controller with something like

    ViewData["ProductList"] = new SelectList(Models.MVCProduct.Instance.ProductList, "Id", "Name", 1);

Then update your View to something like:

    <div class="editor-field">@Html.DropDownList("ProductList")</div>

For the Post/Create/Update step you need to look into the FormCollection to get your results. Reading other posts it sounds like there used to be a bug in here, but it's fixed now so ensure you have the latest. For my example I have a DropDownList for Product so I just get the selected Id and then go searching for that Product in my list.

    [HttpPost]
    public ActionResult Create(FormCollection form )//Models.MVCOrder newOrder)
    {

        MVC.Models.MVCOrder ord = Models.MVCOrder.Instance.CreateBlankOrder();
        //Update order with simple types (e.g. Quantity)
        if (TryUpdateModel<MVC.Models.MVCOrder>(ord, form.ToValueProvider()))
        {
            ord.Product = Models.MVCProduct.Instance.ProductList.Find(p => p.Id == int.Parse(form.GetValue("ProductList").AttemptedValue));
            ord.Attribute = Models.MVCAttribute.Instance.AttributeList.Find(a => a.Id == int.Parse(form.GetValue("attributeId").AttemptedValue));
            UpdateModel(ord);
            return RedirectToAction("Index");
        }
        else
        {
            return View(ord);
        }
    }

I've only been working on MVC3 for the last few days, so any advice on improving the above would be appreciated.

Craig
  • 11
  • 1