locked
Binding custom typed property from DropDownList RRS feed

  • Question

  • User-2127659274 posted

    Hi all,

     what would be the recomended approach to solve this situation. 

    I have a Product class that has a property (Owner) of the type ProductOwner. So for my view I have created a ViewModel-class that holds the Product that is being edited and a list of ProductOwners. That all works fine - a nice list of ProductOwners shows up in the view (by going Html.DropDownList("Owner", Model.ProductOwnersSelectList)).

    Now to the problem - how should I bind to the Owner property? It is of type ProductOwner and the value that the dropdownlist returns is of course simply a string with the selected ID for that ProductOwner. More so, the ModelState.IsValid is false. I presume that this has to do with that the binding has gone wrong. Does the binding by the framework only work for simple types (strings, ints etc.)?

    What would be your recommended approach to solving this?

    Below is the code for the classes involved, in a condensed format.

    Thank you in advance

    /Marcus Hammarberg
    www.marcusoft.net

    public class ProductForm

    {

    public SelectList ProductOwnersSelectList { get; private set; }

    public Product Product { get; private set; }

    public ProductForm(Product product, List<ProductOwner> productOwners)

    {

    Product = product;

    ProductOwnersSelectList = new SelectList(productOwners, "ID", "Name", product.Owner);

    }

    }

    public class Product : BaseEntity

    {

    public string Name { get; set; }

    public string Description { get; set; }

    public ProductOwner Owner { get; set; }

    }

    public class ProductOwner : BaseEntity

    {

    public string Name { get; set; }

    public string EmailAddress { get; set; }

    public string UserName{get; set;}

    }

    Thursday, March 26, 2009 4:36 PM

All replies

  • User2129975138 posted

    Hi Marcus,

    There's a few ways to do what you want:

    1. Inside your ProductForm class, add a Property "OwnerID".
      Inside your you View, name your DropDownList to "OwnerID".
      When you do UpdateModel(productForm) it will bind selected DropDownList Value with OwnerID viewData property.
      Then, you get the Owner object by ID, and update the Product.

      If this approach don't "smell's" good to you, remember that it's the Job of Controller to send and receive View Data. User just selected a Owner, and this information is passed to the Controller as it's ID.
      Then, you have your Domain Objects and Repositories that deal with that. Controller just say: "Ok, user said me that the owner of this Product have the ID 12, so let me search this on my Owners Repository, and associate it with my Product. If something is wrong (this product cannot have this Owner), Domain Objects will say it.
    2. You could name you DropDownList "Product.Owner.ID (I presume you have an ID for ProductOwner - If it's the Name, then call DropDownList "Product.Owner.Name").
      When you do UpdateModel(productForm) it will create the a new Owner Object inside Product, and bind the ID / Name.
      But you will end up with a Product with an Owner Object with just the ID / Name.

      If you want that Binding create all the other Owner properties, you could pass them to the View as hidden inputs (<input type="hidden" name="Product.Owner.UserName value="<%= model.Product.Owner.Username %>" >. Then, at Form Post, Binding will create completely your Owner.
      With this, some malicious user, could change the Hidden inputs, and you will end up with a fake Owner.

      You could also change your Owner Object, so that when you add/change the ID Property, it will update the other properties for this Owner ID (Inside your ID SET - your Product need to have access to your data Repository).

    So, my opinion:

    • I always create a ViewData class, so that I can have Typed Views.
    • Inside my ViewData objects, I put everything that User need to the objective of the View (SelectLists, Names, Options, etc).
    • I collect this information as simple as possible (at least, a ViewData property for every kind of input). At this stage, I don't care about binding my Domain Objects, because I may don't have all the information, and I don't want that my Domain Objects have an invalid state.
    • So, after collecting this information, I call my Factories: when creating, and my Repositories: when updating.
    • All my Domain Objects (Product, Owner) have constructers with all the possible properties that could make a Valid Object.
    • When I ask a Factory for a new Product, I need to tell the to the constructer all the properties necessaries to create a valid Product (a Name, a Suppliers and a Owner). The Owner has been requested to the Owner Repository, and so on ...
    • So, If I have the Rules of creating and getting my Model Objects, I just need to Controller inside Controller the data that I send to the View and how I send it to the Domain Objects. Everything is automatically.
    • In your example, your view have the Objective to associate a Owner to a Product (I assume that every product need to have a Owner since it's creation):
      • I call a Action Method, that have the Product ID as argument
      • Inside my controller I will ask the repository for a Product with that ID. So my ViewData will have a Product.
      • Repository will give me a full Product Object, with a full Owner Object inside him.
      • Because my action method it's about changing the owner, I will ask my Repository for all the Owners. So my ViewData will have a List of Owners.
      • Because I want to know the selected Owner by the user, I will add to my ViewData a String that will store the ID, SelectedOwnerID (I use String because it's the way that HTTP will send me the ID - after I will check if its a valid ID)
      • I send this to the View
      • Its the View how knows how to present this to the user (As DropDownLists, as Radio Buttons, etc). I don't send SelectList's, just Lists. If the View need a SelectList, it will create one with the List of Owners.
      • When user submit, I will collect all the data (I don't use Binding).
      • I will call again my Repository for the product that's being edited.
      • I will call my Repository for a Owner with the ID selected.
      • If I could get the Product and Owner, I will change Product properties as the one's sent by the View and with the new Owner Object. Product Object will say if something is wrong.
      • Then, I send my edited Product to my Repository, and its done.

    Sorry the long Post.

    Hope that this could give you another vision.


    Thursday, March 26, 2009 8:07 PM
  • User-2127659274 posted
    Hi Alberto, and thank you for an excellent answer! If it's good the length doesn't matter. :) The basic idea is completely "home" with me - send the id of the selected ProductOwner and get the complete ProductOwner from a Repository with it. I like that and completly agree with you on the responsibilities. I have also decided to always create a ViewData class and use it for exchanging information between the View and the Controller. It's a bit like contract-first - which I have found great use of. Two questions though: 1 - You write: "I don't do binding" - does that mean that your action methods for updating takes a FormCollection as parameter as in: [AcceptVerbs(HttpVerbs.Post)] public ActionResult Create(FormCollection collection) If you don't do it like that - how do you collect your data? 2 - I really like the idea of a SelectedOwnerID property on the ViewData-class. But I can't get the product to be bound. When I change my Create-action-method into: [AcceptVerbs(HttpVerbs.Post)] public ActionResult Create(ProductForm form ) only the SelectedOwnerID will be set!? Why is form.Product == null? N.B. I have done UpdateModel(form); later on in the code. I even tried to change the name of the fields (in the View) to Product.Name etc. but it still didn't work. The product is not created. The only way I got it to work was with you second suggestion (to name the dropdownbox "Product.Owner.ID" and use that value) - but I don't like it... and from reading between the line you did not like it either. Do I do anything wrong? /Marcus
    Friday, March 27, 2009 10:23 AM
  • User2129975138 posted

    Hi Marcus, 

    - You write: "I don't do binding" - does that mean that your action methods for updating takes a FormCollection as parameter

    Yes, I'm using FormCollection:

    [AcceptVerbs(HttpVerbs.Post)]
            public ActionResult Edit(FormCollection collection)
            {
    
                string selectedOwnerIDViewData = collection["SelectedOwnerID"];
                int selectedOwnerID;
    
                try
                {
                    selectedOwnerID = int.Parse(selectedOwnerIDViewData);
                }
    ...

    My controller say: "Hey Designer Guy, I will send you a list of Owner ID's and its names. I want that you let the user to select the Owner (on the View), and just send me the ID selected with the name "SelectedOwnerID". I do this for all exchanged Data. With this, I know what's happning (with Default Binding, I realised that sometimes I got lost [so I went to Orchidea Station to do a Rollback :) ] ).

    2 - I really like the idea of a SelectedOwnerID property on the ViewData-class. But I can't get the product to be bound.

    Remember that for the view, the Model it's your View Data Class ("ProductForm") and not the Product.

    Inside your View Model you will have a Product, a List of Owners and a string with Selected Owner ID.

    So, in your View, you need to name your DropDownList with "SelectedDropDownList" (and not "Product.SelectedDropDownList" as I suppose you are doing). HTML controls with Product properties, you name "Product.PropertyName ... ".

    Let me know if it worked.

    Friday, March 27, 2009 10:46 AM
  • User-2127659274 posted

    Hi,  

    I will try that - FormCollection and the naming conventions you suggest.

    Just wanted to say that I am sorry about the formatting. This forum doesn't seem to like Chrome very much.

    Thanks for your patience

    /Marcus

    Friday, March 27, 2009 10:57 AM
  • User-2127659274 posted

    OK - that did the trick!

    My complete action for handling the posting of the form now looks like this:

            [AcceptVerbs(HttpVerbs.Post)]

            public ActionResult Create(FormCollection collection)

            {

                var selectedOwnerID = collection["SelectedOwnerID"];

                var product = new Product();

     

                try

                {

                    int ownerID = int.Parse(selectedOwnerID);

                    product.Owner = productOwnerRepository.GetById(ownerID);

     

                    UpdateModel(product, new[] { "Name", "Description" });

     

                    // add new product to the repository

                    productRepository.Add(product);

                    productRepository.Save();

     

                    // Go back to the list

                    return RedirectToAction("Index");

                }

                catch (Exception)

                {

                    throw;

                }

            }

    I like the ViewModel-pattern (is it a pattern?) that we're using right now.

    But I don't like to have to write the name of the properties in quotes (var selectedOwnerID = collection["SelectedOwnerID"], for example). It would have been nice if the framework could have done that for you - as when you declare the Action-method with a parameter of the type that the View is based on. Also that is much easier to use for unit testing.

    Did I get you right there - that kind of automatic binding doesn't always do the trick? Why? Which cases?

    A very big thank you for getting me this far.

    /Marcus

    Friday, March 27, 2009 11:20 AM
  • User2129975138 posted

    Hi Marcus,

    If you are using UpdateModel, you don't need to use FormCollection.

    You could have something like that:

    [AcceptVerbs(HttpVerbs.Post)]
    
            public ActionResult Create(FormCollection collection)
            {
                // Create ViewData
                // It will have:
                // 1 - A Product - Product Product
                // 2 - List of Owners - List&lt;Owners&gt; Owners
                // 3 - Selected Owner ID - string SelectedOwnerID
    
                ProductForm viewData = new ProductForm(); // You could rename this "ProductForm" class to something like "ProductEditViewData".
    
                // You don't need this because Default Bindif will do it for you
                //var selectedOwnerID = collection["SelectedOwnerID"];
                //var product = new Product();
    
                try
                {
                    // UpdateModel will:
                    // - Create a new Product and populate the properties it find with the name "Product.PropertyName"
                    // - Populate SelectedOwnerID string
                    UpdateModel(viewData);
    
                    // Decause you are using UpdateModel, you could declare SelectedOwnerID as
                    // int, and Default Binding will do this validation (and will display
                    // a default validation message)
                    // If so, you don't need this line
                    int ownerID = int.Parse(viewData.SelectedOwnerID); // You could use int.TryParse() to check
    
                    viewData.Product.Owner = productOwnerRepository.GetById(ownerID);
    
                    // add new product to the repository
    
                    productRepository.Add(viewData.Product);
    
                    productRepository.Save();
    
                    // Go back to the list
    
                    return RedirectToAction("Index");
    
                }
                // You need to catch the int.Parse exception to know the difference
                // beteween the UpdateModel exception (or you could use: int.TryParse())
                catch (Exception)
                {
    
                    throw;
    
                }
    
            }

     One note: This way you will have for some moment a Product without a Owner, but you are in the good way. With time, you can refactor it. One possible way:

    • Inside your ProductForm you remove the Product, and add a property of type string for every property of Product.
    • Then, after you get all the view data, and took the Owner object from the Factory you could call a Product Factory like that: 
      ProductFactory.Create(viewData.name, viewData.description, etc, Owner) // "Owner" its an object took by the Repository
    Friday, March 27, 2009 11:53 AM
  • User-2127659274 posted

    Alberto - thank you - That worked very well!

    And I learned a lot by this;

    • To always use a separate ViewModel seems like a very good best practice and is used by WPF also as an recommended approach

    • That the naming of the properties in the view is crucial to get the UpdateModel to work as expected; the "Object.Property"-convention was not apparent for me

    • That UpdateModel helps you with some typing issues (great tip on the string->int conversion for SelectedOwnerID for example)

    Among other stuff 

    So I owe you big time. Thank you so much! And thank you for very pedagogic and clear descriptions

    /Marcus
    www.marcusoft.net

    PS
    I ran into this in a project that I am conducting to learn DDD, TDD and ASP.NET MVC. You can read about it here: http://www.marcusoft.net/search/label/Sprint%20Planner%20Helper.
    Duly praise will go out to you in the close future, in the http://www.marcusoft.net/2009/03/sprint-planner-helper-session-23.html post.

    DS

    Saturday, March 28, 2009 4:41 AM
  • User2129975138 posted

    Hi Marcus,

    Thank You for the good words.

    Its a really pleasure when we can help.

    Hope we will see you doing the same to the community as you learning DDD, TDD and MCV :)

    Regards.

    P.S. - Good name choice for your son ;)

    Saturday, March 28, 2009 9:41 AM