0

I’ve lost more of my hair trying to solve this problem, so I really was hoping you could help?

I have a Telerik MVC grid: Grid in View mode

And I have a custom EditorTemplate for Address (Address.ascx) Grid in Edit mode

My ImporterDetails model:

   public class ImporterViewModel : IEnumerableViewModel
    {
        public int ImporterId { get; set; }

        [Required]
        [DisplayName("Importer Name *")]
        public string ImporterName { get; set; }

        [Required]
        [DisplayName("Importer Address *")]
        public Address ImporterAddress { get; set; }

        public static ImporterViewModel CreateImporter()
        {
            return new ImporterViewModel
                       {
                           ImporterName = Guid.NewGuid().ToString().Substring(0, 5),
                           ImporterAddress = Address.CreateDummyAddress(),
                       };
        }

    }

And the AddressViewModel:

[Bind(Exclude = "State, Country")]
public class Address
{
    public int AddressId { get; set; }

    [DisplayName("Address Line 1")]
    [Required]
    public string Line1 { get; set; }

    [DisplayName("Address Line 2")]
    public string Line2 { get; set; }

    [DisplayName("Postcode")]
    [Required]
    [RegularExpression(RegexConstants.AUSTRALIAN_POSTCODE_PATTERN, ErrorMessage = "Invalid post code")]
    public string Postcode { get; set; }

    [DisplayName("State")]
    public State State { get; set; }

    [DisplayName("Suburb")]
    public string Suburb { get; set; }

    [DisplayName("Country")]
    public Country Country { get; set; }

    [Required]
    public int CountryId { get; set; }

    [Required]
    public int StateId { get; set; }

    /// <summary>
    /// Creates a new dummy instance of Address
    /// </summary>
    /// <returns></returns>
    public static Address CreateDummyAddress()
    {
        return new Address
                   {
                       Country = ServiceLocatorFactory.GetCodeServiceLocator<Country>().Get(x => x.CodeValue.ToLower() == "canada"),
                       State = ServiceLocatorFactory.GetCodeServiceLocator<State>().Get(x => x.CodeValue.ToLower() == "nsw"),
                       Line1 = Guid.NewGuid().ToString().Substring(0, 15),
                       Line2 = Guid.NewGuid().ToString().Substring(0, 15),
                       Suburb = "Dandenong",
                       Postcode = "2606",
                   };
    }

    public string AddressStrings
    {
        get
        {
            return ToString();
        }
    }

    public override string ToString()
    {
        // create a blank StringBuilder
        var sb = new StringBuilder();

        // add the first address line
        sb.Append(string.Format("{0}, ", Line1));

        // add the second address line
        sb.Append(string.Format("{0}, ", Line2));

        sb.Append(string.Format("{0}, ", Suburb));
        sb.Append(string.Format("{0} {1}, ", State == null ? string.Empty : State.Description, Postcode));
        sb.Append(string.Format("{0}", Country == null ? string.Empty : Country.Description));

        // and then return it as a single (formatted) string
        return sb.ToString();
    }
}

You’ll notice I’ve excluded State and Country because if I don’t, when I call a TryUpdateModel(importer) – I get the dreaded parameterless constructor exception. My question is:

How do I go about getting the right id of the State and Country (or in general, any dropdown) in my action this way?

For completeness’ sake:

Address.ascx

<div class="formElementGroupVertical">
    <%: Html.LabelFor(m => m.Line1) %>
    <%: Html.EditorFor(m => m.Line1) %>
    <%: Html.ValidationMessageFor(m => m.Line1) %>
</div>
<div class="formElementGroupVertical">
    <%: Html.LabelFor(m => m.Line2) %>
    <%: Html.EditorFor(m => m.Line2) %>
    <%: Html.ValidationMessageFor(m => m.Line2) %>
</div>
<div class="formElementGroupVertical">
    <%: Html.LabelFor(m => m.Suburb) %>
    <%: Html.EditorFor(m => m.Suburb)%>
    <%: Html.ValidationMessageFor(m => m.Suburb)%>
</div>
<div class="formElementGroupVertical">
    <%: Html.LabelFor(m => m.State) %>
    <%: Html.EditorFor(m => m.State) %>
</div>
<div class="formElementGroupVertical">
    <%: Html.LabelFor(m => m.Postcode) %>
    <%: Html.EditorFor(m => m.Postcode)%>
    <%: Html.ValidationMessageFor(m => m.Postcode)%>
</div>
<div class="formElementGroupVertical">
    <%: Html.LabelFor(m => m.Country) %>
    <%: Html.EditorFor(m => m.Country) %>
</div>

Country:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Web.Common.Models.Country>" %>
<%@ Import Namespace="Web.Common.Models" %>
<%@ Import Namespace="Web.Common.Service" %>
<%: Html.DropDownListFor(m => m.CodeId, new SelectList(ServiceLocatorFactory.GetCodeServiceLocator<Country>().GetAll(), "CodeId", "Description"), "Please Select")%>

And state is Identical to Country except the obvious.

Any help?

2 Answers 2

2

Short Answer:

CountryId is not being populated because the DropDownlistFor is 'For' country => country.CodeId.

To get CountryId, you'd actually need to point the dropdown list to it:

Html.DropDownListFor(m => m.CountryId, new SelectList(ServiceLocatorFactory.GetCodeServiceLocator<Country>().GetAll(), "CodeId", "Description"), "Please Select")%>

Slightly longer answer:

The easiest way to get a value of a dropdown is bind DropDownListFor to a property on your viewmodel that stores the id. Then in your controller you'd generate (e.g. via the database) the object from that id and attach it to whatever model as per your business requirements.

It's troublesome but straight-forward. There's currently no automatic way to modelbind full objects via dropdowns AFAIK.

In your case, your viewmodel would have:

public class AddressViewModel
{
  public int SelectedCountryId { get; set; }
}

then use the DropDownFor() in this way:

<%: Html.DropDownListFor(m => m.SelectedCountryId, new SelectList(ServiceLocatorFactory.GetCodeServiceLocator<Country>().GetAll(), "CodeId", "Description"), "Please Select")%>

And then in your Action (in pseudo-code):

public ViewResult Save(AddressViewModel addressVM)
{
  var address = new Address() { Country = countriesStore.ById(addressVM.SelectedCountryId) };
  address.Save();

  ...
}

This way also means that you'd need to use a view model instead of the domain model for your views. Your example seems to be using the domain model so you might want to look into changing this. Once you start using view models you'll also need something like Automapper to facilitate mapping your view models to domain models.

Sign up to request clarification or add additional context in comments.

3 Comments

Hi, thank you for this! First off, these are actually pure View models only and are not domain models or DTOs. Secondly, I already have the CountryId and StateId as a property as well. You'll note the [Bind(Exclude = "State, Country")] at the top of the ViewModel. This allows the model to be updated successfully, but, I don't get anything in the CountryId or StateId fields either; which is what I was trying to ask. Of course, in hindsight, my question doesn't seem very clear.
Oh my bad too. I understood the question but I didn't actually answer it directly. I've updated my answer anyway in case some else needs it.
Thank you, this is much more descriptive and I think I understand what you're now saying too. This I must implement asap :)
0

Okay folks, I seem to have a temporary workaround to my problem, and would love to know your feedback, and possibly any other alternatives that may make this solution more "robust".

In my action, I check the Request.Form and lookup the dropdown specified by the PropertyName of the complex type. So the syntax looks like this:

PropertyName.PropertyName.Value field being returned by the dropdown. Using this, I am then able to look up the repository if required to get the instance of the ComplexType.

My Action is pasted below for reference:

[AcceptVerbs(HttpVerbs.Post)]
[GridAction]
public ActionResult Update(int id)
{
    // obtain the instance of the importer we wish to update
    var importer = serviceLocator.Get(i => i.ImporterId == id);

    // the address object doesn't bind the State and Country dropdowns so we must manually read these
    var stateCodeId = Request.Form["ImporterAddress.State.CodeId"];
    var countryCodeId = Request.Form["ImporterAddress.Country.CodeId"];

    //Perform model binding (fill the properties and validate the model)
    if (TryUpdateModel(importer))
    {
        // parse the Id fields of the selected values of the dropdowns that we've just read from the request objects
        importer.ImporterAddress.StateId = int.Parse(stateCodeId);
        importer.ImporterAddress.CountryId = int.Parse(countryCodeId);

        // and convert them to their specific code objects
        importer.ImporterAddress.State =
            ServiceLocatorFactory.GetCodeServiceLocator<State>().Get(s => s.CodeId == int.Parse(stateCodeId));
        importer.ImporterAddress.Country =
            ServiceLocatorFactory.GetCodeServiceLocator<Country>().Get(s => s.CodeId == int.Parse(countryCodeId));

        // now save the updated model
        serviceLocator.Update(importer);
    }

    // rebind the grid
    return PartialView(new GridModel(serviceLocator.GetAll().ToList()));
}

2 Comments

Seems this is conceptually similar to my solution above. The problem for me is the very brittle strings in Request.Form - I'd rather do Html.DropDownListFor(m => m.SelectedCountryId ... and get type-safe properties in my actions.
You're right, but I believe your solution is much more elegant.

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.