Модель ASP.NET MVC, связывающая отношение внешнего ключа - PullRequest
7 голосов
/ 19 марта 2009

Можно ли связать отношение внешнего ключа в моей модели с вводом формы?

Скажем, у меня отношения один-ко-многим между Car и Manufacturer. Я хочу, чтобы у меня была форма для обновления Car, включающая входные данные для настройки Manufacturer. Я надеялся, что смогу сделать это, используя привязку встроенной модели, но начинаю думать, что мне придется сделать это самому.

Моя подпись метода действия выглядит следующим образом:

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

Форма публикует значения Имя, Описание и Производитель, где Производитель является первичным ключом типа int. Имя и описание устанавливаются правильно, но не «Производитель», что имеет смысл, так как связыватель модели не имеет представления о поле PK. Значит ли это, что мне нужно написать кастом IModelBinder, чтобы он знал об этом? Я не уверен, как это будет работать, так как мои хранилища доступа к данным загружаются через контейнер IoC в каждом конструкторе Controller.

Ответы [ 3 ]

6 голосов
/ 04 июня 2010

Вот мое мнение: это пользовательский механизм связывания модели, который при запросе GetPropertyValue проверяет, является ли свойство объектом из моей сборки модели и имеет ли IRepository <> зарегистрированный в моем NInject IKernel. Если он может получить IRepository от Ninject, он использует его для извлечения объекта внешнего ключа.

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>();
    }

}

Вы, очевидно, могли бы заменить Ninject своим DI-контейнером и собственным типом хранилища.

3 голосов
/ 19 марта 2009

Конечно, у каждого автомобиля есть только один производитель. Если это так, то у вас должно быть поле ManufacturerID, к которому вы можете привязать значение выбора. То есть ваш выбор должен иметь имя производителя в качестве текста и идентификатор в качестве значения. В качестве значения сохранения свяжите идентификатор производителя, а не производителя.

<%= Html.DropDownList( "ManufacturerID",
        (IEnumerable<SelectListItem>)ViewData["Manufacturers"] ) %>

С

ViewData["Manufacturers"] = db.Manufacturers
                              .Select( m => new SelectListItem
                                            {
                                               Text = m.Name,
                                               Value = m.ManufacturerID
                                            } )
                               .ToList();

И

public JsonResult Save(int id,
                       [Bind(Include="Name, Description, ManufacturerID")]Car car)
2 голосов
/ 08 апреля 2010

Возможно, это уже поздно, но вы можете использовать специальное связующее для достижения этой цели. Обычно я делал бы это так же, как @tvanofosson, но у меня был случай, когда я добавлял UserDetails в таблицы AspNetMembershipProvider. Поскольку я также использую только POCO (и отображаю его из EntityFramework), я не хотел использовать идентификатор, потому что он не был оправдан с точки зрения бизнеса, поэтому я создал модель только для добавления / регистрации пользователей. Эта модель имеет все свойства для пользователя, а также свойство Role. Я хотел связать текстовое имя роли с ее представлением RoleModel. Вот в основном то, что я сделал:

public class RoleModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        string roleName = controllerContext.HttpContext.Request["Role"];

        var model = new RoleModel
                          {
                              RoleName = roleName
                          };

        return model;
    }
}

Затем мне пришлось добавить следующее в Global.asax:

ModelBinders.Binders.Add(typeof(RoleModel), new RoleModelBinder());

И использование в представлении:

<%= Html.DropDownListFor(model => model.Role, new SelectList(Model.Roles, "RoleName", "RoleName", Model.Role))%>

Надеюсь, это поможет вам.

...