简体   繁体   中英

Model Binding with Parent/Child Relationship

I'm sure this has been answered before, but I've spent the last three hours looking for an acceptable solution and have been unable to find anything, so I apologize for what I'm sure is a repeat.

I have two domain objects, Player and Position. Player's have a Position. My domain objects are POCOs tied to my database with NHibernate. I have an Add action that takes a Player, so I'm using the built in model binding. On my view I have a drop down list that lets a user select the Position for the Player. The value of the drop down list is the Id of the position. Everything gets populated correctly except that my Position object fails validation (ModelState.IsValid) because at the point of model binding it only has an Id and none of it's other required attributes.

What is the preferred solution for solving this with ASP.NET MVC 2?

Solutions I've tried...

  1. Fetch the Position from the database based on the Id before ModelState.IsValid is called in the Add action of my controller. I can't get the model to run the validation again, so ModelState.IsValid always returns false.
  2. Create a custom ModelBinder that inherits from the default binder and fetch the Position from the database after the base binder is called. The ModelBinder seems to be doing the validation so if I use anything from the default binder I'm hosed. Which means I have to completely roll my own binder and grab every value from the form...this seems really wrong and inefficient for such a common use-case.

Solutions I think might work, I just can't figure out how to do...

  1. Turn off the validation for the Position class when used in Player.
  2. Write a custom ModelBinder leverages the default binder for most of the property binding, but lets me get the Position from the database BEFORE the default binder runs validation.

So, how do the rest of you solve this?

Thanks,

Dan

PS In my opinion having a PositionId on Player just for this case is not a good solution. There has to be solvable in a more elegant fashion.

Not only to this particular problem, but in general, I would create a separate ViewModel instead of letting the view have the domain model. So, in your case, you don't need to overexpose your domain model (and getting things that you don't needed) to the view, and, nevertheless, turning off validation is most probably the worst solution

创建一个可以为您验证此特殊情况的自定义ModelBinder。

I have had the same issue in one of my applications. I solved it by creating a custom IModelBinder inheriting from DefaultModelBinder and registered it with the particular type I'm binding.

The trick is to use session.Load<> which only creates a typed reference to the entity without querying the database. ( Read more about session.Load here in Ayende's blog )

/// <summary>
    /// Base for binding references. Usually displayed in dropdowns
    /// </summary>
    public class ReferenceBinder<T> : DefaultModelBinder
        where T : class
    {

        private readonly ISession session;

        public ReferenceBinder(ISession session)
        {
            this.session = session;
        }


        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {

            var idName = CreateSubPropertyName(bindingContext.ModelName, "ID");

            ValueProviderResult result = bindingContext.ValueProvider.GetValue(idName);

            int value;
            return (int.TryParse(result.AttemptedValue, out value)) ?  this.session.Load<T>(value) : null;

        }


    }

In your example you would do something like this in Global.asax:

ModelBinders.Binders.Add(typeof(Position), new ReferenceBinder<Position>(<pass your current session implementation or use DI/IoC>));

(I'm using Castle Windsor to create the actual binder and populate session in my implementation)

Assuming your domain model is:

        public class Player {
            public virtual Position Position {get;set;}
        }

        public class Position {
            public virtual int ID {get;private set;}
        }

And your postback looks like this:

"Position.ID" = <id from dropdown>

EDIT: You should probably use a DTO approach with dedicated view models. I've learned this later on. Have a look here for a quick start .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM