Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC model binding foreign key relationship

Is it possible to bind a foreign key relationship on my model to a form input?

Say I have a one-to-many relationship between Car and Manufacturer. I want to have a form for updating Car that includes a select input for setting Manufacturer. I was hoping to be able to do this using the built-in model binding, but I'm starting to think I'll have to do it myself.

My action method signature looks like this:

public JsonResult Save(int id, [Bind(Include="Name, Description, Manufacturer")]Car car)

The form posts the values Name, Description and Manufacturer, where Manufacturer is a primary key of type int. Name and Description get set properly, but not Manufacturer, which makes sense since the model binder has no idea what the PK field is. Does that mean I would have to write a custom IModelBinder that it aware of this? I'm not sure how that would work since my data access repositories are loaded through an IoC container on each Controller constructor.

like image 815
roryf Avatar asked Mar 19 '09 15:03

roryf


People also ask

How can add ForeignKey in MVC model?

To create Foreign Key, you need to use ForeignKey attribute with specifying the name of the property as parameter. You also need to specify the name of the table which is going to participate in relationship.

How does model binding works in MVC?

ASP.NET MVC model binding allows mapping HTTP request data with a model. It is the procedure of creating . NET objects using the data sent by the browser in an HTTP request. Model binding is a well-designed bridge between the HTTP request and the C# action methods.

Does MVC use data binding?

asp.net mvc supports data binding. You can bind data to some model and post it back when the form is submitted.

Is MVC model binding case sensitive?

MVC framework automatically converts a query string to the action method parameters provided their names are matching. For example, the query string id in the following GET request would automatically be mapped to the Edit() action method's id parameter. This binding is case insensitive.


1 Answers

Here's my take - this is a custom model binder that when asked to GetPropertyValue, looks to see if the property is an object from my model assembly, and has a IRepository<> registered in my NInject IKernel. If it can get the IRepository from Ninject, it uses that to retrieve the foreign key object.

public class ForeignKeyModelBinder : System.Web.Mvc.DefaultModelBinder
{
    private IKernel serviceLocator;

    public ForeignKeyModelBinder( IKernel serviceLocator )
    {
        Check.Require( serviceLocator, "IKernel is required" );
        this.serviceLocator = serviceLocator;
    }

    /// <summary>
    /// if the property type being asked for has a IRepository registered in the service locator,
    /// use that to retrieve the instance.  if not, use the default behavior.
    /// </summary>
    protected override object GetPropertyValue( ControllerContext controllerContext, ModelBindingContext bindingContext,
        PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder )
    {
        var submittedValue = bindingContext.ValueProvider.GetValue( bindingContext.ModelName );
        if ( submittedValue == null )
        {
            string fullPropertyKey = CreateSubPropertyName( bindingContext.ModelName, "Id" );
            submittedValue = bindingContext.ValueProvider.GetValue( fullPropertyKey );
        }

        if ( submittedValue != null )
        {
            var value = TryGetFromRepository( submittedValue.AttemptedValue, propertyDescriptor.PropertyType );

            if ( value != null )
                return value;
        }

        return base.GetPropertyValue( controllerContext, bindingContext, propertyDescriptor, propertyBinder );
    }

    protected override object CreateModel( ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType )
    {
        string fullPropertyKey = CreateSubPropertyName( bindingContext.ModelName, "Id" );
        var submittedValue = bindingContext.ValueProvider.GetValue( fullPropertyKey );
        if ( submittedValue != null )
        {
            var value = TryGetFromRepository( submittedValue.AttemptedValue, modelType );

            if ( value != null )
                return value;
        }

        return base.CreateModel( controllerContext, bindingContext, modelType );
    }

    private object TryGetFromRepository( string key, Type propertyType )
    {
        if ( CheckRepository( propertyType ) && !string.IsNullOrEmpty( key ) )
        {
            Type genericRepositoryType = typeof( IRepository<> );
            Type specificRepositoryType = genericRepositoryType.MakeGenericType( propertyType );

            var repository = serviceLocator.TryGet( specificRepositoryType );
            int id = 0;
#if DEBUG
            Check.Require( repository, "{0} is not available for use in binding".FormatWith( specificRepositoryType.FullName ) );
#endif
            if ( repository != null && Int32.TryParse( key, out id ) )
            {
                return repository.InvokeMethod( "GetById", id );
            }
        }

        return null;
    }

    /// <summary>
    /// perform simple check to see if we should even bother looking for a repository
    /// </summary>
    private bool CheckRepository( Type propertyType )
    {
        return propertyType.HasInterface<IModelObject>();
    }

}

you could obviously substitute Ninject for your DI container and your own repository type.

like image 140
Dave Thieben Avatar answered Oct 13 '22 13:10

Dave Thieben