views:

124

answers:

2

Hi i have an entity called User with 2 properties called UserName and Role (which is a reference to another entity called Role). I'm trying to update the UserName and RoleID from a form which is posted back. Within my postback action i have the following code:

var user = new User();

TryUpdateModel(user, "User", new string[] { "UserName, Role.RoleID" });

TryUpdateModel(user, new string[] { "User.UserName, User.Role.RoleID" });

However none of these updates the Role.RoleID property. If i try the following:

TryUpdateModel(user, "User", new string[] { "UserName, Role" });

TryUpdateModel(user);

The RoleID is updated but the RoleName property is validated aswell. That's why i'm trying to be more specific on which properties to update but i can't get any of the first examples to work.

I'd appreciate if someone could help. Thanks

A: 

Consider calling TryUpdateModel twice, once for the User. Once for the Role. Then assign the role to the user.

var user = new User();
var role = new Role();

TryUpdateModel(user, new string[] { "UserName" });
TryUpdateModel(role, new string[] { "RoleID" });

user.Role = role;

See if that works.

Haacked
Hi thanks for your answer but i've managed to come up with something that saves me having to call TryUpdateModel multiple times.
nfplee
+1  A: 

Here's a complete solution to work with any relationship.

First place the following line of code in your Application_Start event:

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

Now you need to add the following class somewhere in your application:

public class CustomModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (bindingContext.ModelType.Namespace.EndsWith("Models.Entities") && !bindingContext.ModelType.IsEnum && value != null)
        {
            if (Utilities.IsInteger(value.AttemptedValue))
            {
                var repository = ServiceLocator.Current.GetInstance(typeof(IRepository<>).MakeGenericType(bindingContext.ModelType));
                return repository.GetType().InvokeMember("GetByID", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public, null, repository, new object[] { Convert.ToInt32(value.AttemptedValue) });
            }
            else if (value.AttemptedValue == "")
                return null;
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}

Please note the above code may need to be modified to suit your needs. It affectively calls IRepository().GetByID(???). It will work when binding against any entities within the Models.Entities namespace and that have an integer value.

Now for the view there's one other bit of work you have to do to fix a bug in ASP.NET MVC 2. By default the Selected property on a SelectListItem is ignored so i have come up with my own DropDownListFor which allows you to pass in the selected value.

public static class SelectExtensions
{
    public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel)
    {
        return DropDownListFor(helper, expression, selectList, selectedValue, optionLabel, null);
    }

    public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel, object htmlAttributes)
    {
        return DropDownListHelper(helper, ExpressionHelper.GetExpressionText(expression), selectList, selectedValue, optionLabel, new RouteValueDictionary(htmlAttributes));
    }

    /// <summary>
    /// This is almost identical to the one in ASP.NET MVC 2 however it removes the default values stuff so that the Selected property of the SelectListItem class actually works
    /// </summary>
    private static MvcHtmlString DropDownListHelper(HtmlHelper helper, string name, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel, IDictionary<string, object> htmlAttributes)
    {
        name = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        // Convert each ListItem to an option tag
        var listItemBuilder = new StringBuilder();

        // Make optionLabel the first item that gets rendered
        if (optionLabel != null)
            listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false }, selectedValue));

        // Add the other options
        foreach (var item in selectList)
        {
            listItemBuilder.AppendLine(ListItemToOption(item, selectedValue));
        }

        // Now add the select tag
        var tag = new TagBuilder("select") { InnerHtml = listItemBuilder.ToString() };
        tag.MergeAttributes(htmlAttributes);
        tag.MergeAttribute("name", name, true);
        tag.GenerateId(name);

        // If there are any errors for a named field, we add the css attribute
        ModelState modelState;

        if (helper.ViewData.ModelState.TryGetValue(name, out modelState))
        {
            if (modelState.Errors.Count > 0)
                tag.AddCssClass(HtmlHelper.ValidationInputCssClassName);
        }

        return tag.ToMvcHtmlString(TagRenderMode.Normal);
    }

    internal static string ListItemToOption(SelectListItem item, string selectedValue)
    {
        var tag = new TagBuilder("option") { InnerHtml = HttpUtility.HtmlEncode(item.Text) };

        if (item.Value != null)
            tag.Attributes["value"] = item.Value;

        if ((!string.IsNullOrEmpty(selectedValue) && item.Value == selectedValue) || item.Selected)
            tag.Attributes["selected"] = "selected";

        return tag.ToString(TagRenderMode.Normal);
    }
}

Now within your view you can say:

<%= Html.DropDownListFor(m => m.User.Role, Model.Roles, Model.User.Role != null ? Model.User.Role.RoleID.ToString() : "", "-- Please Select --")%>
<%= Html.ValidationMessageFor(m => m.User.Role, "*")%>

The Role property will automatically update when you call TryUpdateModel in your controller and you have to do no additional work to wire this up. Although it's alot of code initially i've found this approach saves heaps of code in the long term.

Hope this helps.

nfplee