Модель MVC 3, связывающая подтип (абстрактный класс или интерфейс) - PullRequest
48 голосов
/ 23 февраля 2012

Скажем, у меня есть модель Product, у модели Product есть свойство ProductSubType (аннотация), и у нас есть две конкретные реализации Shirt и Pants.

Вот источник:

 public class Product
 {
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public decimal? Price { get; set; }

    [Required]
    public int? ProductType { get; set; }

    public ProductTypeBase SubProduct { get; set; }
}

public abstract class ProductTypeBase { }

public class Shirt : ProductTypeBase
{
    [Required]
    public string Color { get; set; }
    public bool HasSleeves { get; set; }
}

public class Pants : ProductTypeBase
{
    [Required]
    public string Color { get; set; }
    [Required]
    public string Size { get; set; }
}

В моем пользовательском интерфейсе у пользователя есть выпадающий список, он может выбрать тип продукта, а элементы ввода отображаются в соответствии с нужным типом продукта. Я все это выяснил (используя ajax, включите выпадающее меню, верните шаблон части / редактора и переустановите проверку jquery соответствующим образом).

Далее я создал привязку пользовательской модели для ProductTypeBase.

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

        ProductTypeBase subType = null;

        var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));

        if (productType == 1)
        {
            var shirt = new Shirt();

            shirt.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string));
            shirt.HasSleeves = (bool)bindingContext.ValueProvider.GetValue("SubProduct.HasSleeves").ConvertTo(typeof(bool));

            subType = shirt;
        }
        else if (productType == 2)
        {
            var pants = new Pants();

            pants.Size = (string)bindingContext.ValueProvider.GetValue("SubProduct.Size").ConvertTo(typeof(string));
            pants.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string));

            subType = pants;
        }

        return subType;

    }
}

Это правильно связывает значения и работает по большей части, за исключением того, что я теряю проверку на стороне сервера. Итак, догадываясь, что я делаю это неправильно, я провел еще несколько поисков и наткнулся на ответ Дарина Димитрова:

ASP.NET MVC 2 - привязка к абстрактной модели

Итак, я переключил связыватель модели, чтобы он переопределял только CreateModel, но теперь он не связывает значения.

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        ProductTypeBase subType = null;

        var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));

        if (productType == 1)
        {
            subType = new Shirt();
        }
        else if (productType == 2)
        {
            subType = new Pants();
        }

        return subType;
    }

Если выполнить MVC 3 src, похоже, что в BindProperties GetFilteredModelProperties возвращает пустой результат, и я думаю, это потому, что для модели bindingcontext задано значение ProductTypeBase, которое не имеет никаких свойств.

Может кто-нибудь заметить, что я делаю не так? Это не похоже, что это должно быть так сложно. Я уверен, что упускаю что-то простое ... У меня есть другая альтернатива, вместо того, чтобы вместо свойства SubProduct в модели Product иметь отдельные свойства для рубашки и штанов. Это просто модели View / Form, так что я думаю, что это сработает, но хотелось бы, чтобы текущий подход работал, если что-нибудь понимает, что происходит ...

Спасибо за любую помощь!

Обновление:

Я не прояснил, но добавленный мной пользовательский механизм связывания наследуется от DefaultModelBinder

Ответ

Установка ModelMetadata и Model была отсутствующей частью. Спасибо, Манас!

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            if (modelType.Equals(typeof(ProductTypeBase))) {
                Type instantiationType = null;

                var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));

                if (productType == 1) {
                    instantiationType = typeof(Shirt);
                }
                else if (productType == 2) {
                    instantiationType = typeof(Pants);
                }

                var obj = Activator.CreateInstance(instantiationType);
                bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
                bindingContext.ModelMetadata.Model = obj;
                return obj;
            }

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

        }

Ответы [ 3 ]

57 голосов
/ 24 февраля 2012

Это может быть достигнуто путем переопределения CreateModel (...).Я продемонстрирую это на примере.

1.Давайте создадим модель и несколько базовых и дочерних классов .

public class MyModel
{
    public MyBaseClass BaseClass { get; set; }
}

public abstract class MyBaseClass
{
    public virtual string MyName
    {
        get
        {
            return "MyBaseClass";
        }
    }
}

public class MyDerievedClass : MyBaseClass
{

    public int MyProperty { get; set; }
    public override string MyName
    {
        get
        {
            return "MyDerievedClass";
        }
    }
}

2.Теперь создайте привязку модели и переопределите CreateModel

public class MyModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        /// MyBaseClass and MyDerievedClass are hardcoded.
        /// We can use reflection to read the assembly and get concrete types of any base type
        if (modelType.Equals(typeof(MyBaseClass)))
        {
            Type instantiationType = typeof(MyDerievedClass);                
            var obj=Activator.CreateInstance(instantiationType);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
            bindingContext.ModelMetadata.Model = obj;
            return obj;
        }
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }

}

3.Теперь в контроллере создайте действия get и post.

[HttpGet]
public ActionResult Index()
    {
        ViewBag.Message = "Welcome to ASP.NET MVC!";

        MyModel model = new MyModel();
        model.BaseClass = new MyDerievedClass();

        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyModel model)
    {

        return View(model);
    }

4.Теперь установите MyModelBinder в качестве ModelBinder по умолчанию в global.asax Это сделано, чтобы установить связыватель модели по умолчанию для всех действий, для одного действия мы можем использовать атрибут ModelBinder в параметрах действия)

protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        ModelBinders.Binders.DefaultBinder = new MyModelBinder();

        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);
    }

5.Теперь мы можем создать представление типа MyModel и частичное представление типа MyDertainedClass

Index.cshtml

@model MvcApplication2.Models.MyModel

@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>

@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
    <legend>MyModel</legend>
    @Html.EditorFor(m=>m.BaseClass,"DerievedView")
    <p>
        <input type="submit" value="Create" />
    </p>
</fieldset>
}

DertainedView.cshtml

@model MvcApplication2.Models.MyDerievedClass

@Html.ValidationSummary(true)
<fieldset>
    <legend>MyDerievedClass</legend>

    <div class="editor-label">
        @Html.LabelFor(model => model.MyProperty)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.MyProperty)
        @Html.ValidationMessageFor(model => model.MyProperty)
    </div>

</fieldset>

Теперь все будет работать как положено, контроллер получит объект типа «MyDereveClass».Валидация произойдет, как и ожидалось.

enter image description here

4 голосов
/ 06 марта 2012

У меня была та же проблема, я в итоге использовал MvcContrib в качестве sugested здесь .

Документация устарела, но если вы посмотритев примерах это довольно просто.

Вам необходимо зарегистрировать ваши типы в Global.asax:

protected void Application_Start(object sender, EventArgs e) {
    // (...)
    DerivedTypeModelBinderCache.RegisterDerivedTypes(typeof(ProductTypeBase), new[] { typeof(Shirt), typeof(Pants) });
}

Добавить две строки в ваши частичные представления:

@model MvcApplication.Models.Shirt
@using MvcContrib.UI.DerivedTypeModelBinder
@Html.TypeStamp()
<div>
    @Html.LabelFor(m => m.Color)
</div>
<div>
    @Html.EditorFor(m => m.Color)
    @Html.ValidationMessageFor(m => m.Color)
</div>

Наконец, на главном экране (с использованием EditorTemplates ):

@model MvcApplication.Models.Product
@{
    ViewBag.Title = "Products";
}
<h2>
    @ViewBag.Title</h2>

@using (Html.BeginForm()) {
    <div>
        @Html.LabelFor(m => m.Name)
    </div>
    <div>
        @Html.EditorFor(m => m.Name)
        @Html.ValidationMessageFor(m => m.Name)
    </div>
    <div>
        @Html.EditorFor(m => m.SubProduct)
    </div>
    <p>
        <input type="submit" value="create" />
    </p>
}
1 голос
/ 12 января 2015

хорошо У меня была такая же проблема, и я решил более общим способом, я думаю. В моем случае я отправляю объект через Json из бэкэнда клиенту и из клиента в бэкэнд:

Прежде всего, в абстрактном классе у меня есть поле, которое я установил в конструкторе:

ClassDescriptor = this.GetType().AssemblyQualifiedName;

Итак, в Json у меня есть поле ClassDescriptor

Следующим шагом было написание пользовательского связующего:

public class SmartClassBinder : DefaultModelBinder
{
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {

            string field = String.Join(".", new String[]{bindingContext.ModelName ,  "ClassDescriptor"} );
                var values = (ValueProviderCollection) bindingContext.ValueProvider;
                var classDescription = (string) values.GetValue(field).ConvertTo(typeof (string));
                modelType = Type.GetType(classDescription);

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

И теперь все, что мне нужно сделать, это украсить класс атрибутом. Например:

[ModelBinder (TypeOf (SmartClassBinder))] открытый класс ConfigurationItemDescription

Вот и все.

...