Nice programing

다형성 모델 바인딩

nicepro 2020. 12. 30. 20:24
반응형

다형성 모델 바인딩


이 질문은 이전 버전의 MVC에서 요청 되었습니다 . 문제를 해결하는 방법에 대한 이 블로그 항목있습니다. MVC3가 도움이 될만한 것을 도입했는지 또는 다른 옵션이 있는지 궁금합니다.

간단히 말해서. 여기 상황이 있습니다. 추상 기본 모델과 2 개의 구체적인 하위 클래스가 있습니다. .NET Core로 모델을 렌더링하는 강력한 형식의 뷰가 EditorForModel()있습니다. 그런 다음 각 구체적인 유형을 렌더링하는 사용자 지정 템플릿이 있습니다.

문제는 포스트 타임에 발생합니다. 포스트 액션 메서드가 기본 클래스를 매개 변수로 사용하도록하면 MVC는 추상 버전을 만들 수 없습니다 (어쨌든 원하지 않는 실제 구체적인 형식을 만들고 싶습니다). 매개 변수 서명에 의해서만 달라지는 여러 포스트 작업 메서드를 만들면 MVC가 모호하다고 불평합니다.

내가 말할 수있는 한,이 문제를 해결하는 방법에 대한 몇 가지 선택 사항이 있습니다. 여러 가지 이유로 좋아하지 않지만 여기에 나열하겠습니다.

  1. 내가 링크 한 첫 번째 게시물에서 Darin이 제안한대로 사용자 지정 모델 바인더를 만듭니다.
  2. 내가 링크 한 두 번째 게시물이 제안한대로 판별 자 속성을 작성하십시오.
  3. 유형에 따라 다른 작업 방법에 게시
  4. ???

기본적으로 숨겨진 구성이기 때문에 1을 좋아하지 않습니다. 코드를 작업하는 일부 다른 개발자는 그것에 대해 알지 못하고 변경 될 때 문제가 발생하는 이유를 파악하는 데 많은 시간을 낭비 할 수 있습니다.

나는 2가 싫다. 하지만 저는이 접근 방식에 기대고 있습니다.

DRY 위반을 의미하기 때문에 3을 좋아하지 않습니다.

다른 제안이 있습니까?

편집하다:

나는 Darin의 방법을 사용하기로 결정했지만 약간 변경했습니다. 나는 이것을 내 추상 모델에 추가했습니다.

[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}

그런 다음 숨겨진 파일이 자동으로 생성됩니다 DisplayForModel(). 기억해야 할 것은를 사용하지 않는 DisplayForModel()경우 직접 추가해야한다는 것입니다.


분명히 옵션 1 (:-))을 선택했기 때문에 좀 더 자세히 설명하여 깨지기 쉬우 며 구체적인 인스턴스를 모델 바인더에 하드 코딩하지 않도록 하겠습니다 . 아이디어는 구체적인 유형을 숨겨진 필드에 전달하고 반사를 사용하여 구체적인 유형을 인스턴스화하는 것입니다.

다음 뷰 모델이 있다고 가정합니다.

public abstract class BaseViewModel
{
    public int Id { get; set; }
}

public class FooViewModel : BaseViewModel
{
    public string Foo { get; set; }
}

다음 컨트롤러 :

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new FooViewModel { Id = 1, Foo = "foo" };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(BaseViewModel model)
    {
        return View(model);
    }
}

해당 Index보기 :

@model BaseViewModel
@using (Html.BeginForm())
{
    @Html.Hidden("ModelType", Model.GetType())    
    @Html.EditorForModel()
    <input type="submit" value="OK" />
}

~/Views/Home/EditorTemplates/FooViewModel.cshtml편집기 템플릿 :

@model FooViewModel
@Html.EditorFor(x => x.Id)
@Html.EditorFor(x => x.Foo)

이제 다음과 같은 사용자 지정 모델 바인더를 사용할 수 있습니다.

public class BaseViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
        var type = Type.GetType(
            (string)typeValue.ConvertTo(typeof(string)),
            true
        );
        if (!typeof(BaseViewModel).IsAssignableFrom(type))
        {
            throw new InvalidOperationException("Bad Type");
        }
        var model = Activator.CreateInstance(type);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
        return model;
    }
}

실제 유형은 ModelType숨겨진 필드 의 값에서 유추됩니다 . 하드 코딩되지 않으므로 나중에이 모델 바인더를 건드리지 않고도 다른 자식 유형을 추가 할 수 있습니다.

이 동일한 기술 을 기본 뷰 모델 컬렉션에 쉽게 적용 할 수 있습니다 .


이 문제에 대한 흥미로운 해결책을 방금 생각했습니다. 다음과 같이 Parameter bsed 모델 바인딩을 사용하는 대신 :

[HttpPost]
public ActionResult Index(MyModel model) {...}

대신 TryUpdateModel ()을 사용하여 코드에서 바인딩 할 모델의 종류를 결정할 수 있습니다. 예를 들어 다음과 같이합니다.

[HttpPost]
public ActionResult Index() {...}
{
    MyModel model;
    if (ViewData.SomeData == Something) {
        model = new MyDerivedModel();
    } else {
        model = new MyOtherDerivedModel();
    }

    TryUpdateModel(model);

    if (Model.IsValid) {...}

    return View(model);
}

어쨌든 이것은 실제로 훨씬 더 잘 작동합니다. 왜냐하면 내가 어떤 처리를하고 있다면, 모델을 실제로 그것이 무엇이든간에 캐스트하거나 isAutoMapper로 호출 할 올바른 맵을 알아내는 데 사용해야 하기 때문입니다.

나는 잊어 1 일부터 MVC를 사용하지 않은 사람들을 생각 UpdateModel등을 TryUpdateModel,하지만 여전히 그 용도가있다.


It took me a good day to come up with an answer to a closely related problem - although I'm not sure it's precisely the same issue, I'm posting it here in case others are looking for a solution to the same exact problem.

In my case, I have an abstract base-type for a number of different view-model types. So in the main view-model, I have a property of an abstract base-type:

class View
{
    public AbstractBaseItemView ItemView { get; set; }
}

I have a number of sub-types of AbstractBaseItemView, many of which define their own exclusive properties.

My problem is, the model-binder does not look at the type of object attached to View.ItemView, but instead looks only at the declared property-type, which is AbstractBaseItemView - and decides to bind only the properties defined in the abstract type, ignoring properties specific to the concrete type of AbstractBaseItemView that happens to be in use.

The work-around for this isn't pretty:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

// ...

public class ModelBinder : DefaultModelBinder
{
    // ...

    override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null)
        {
            var concreteType = bindingContext.Model.GetType();

            if (Nullable.GetUnderlyingType(concreteType) == null)
            {
                return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType);
            }
        }

        return base.GetTypeDescriptor(controllerContext, bindingContext);
    }

    // ...
}

Although this change feels hacky and is very "systemic", it seems to work - and does not, as far as I can figure, pose a considerable security-risk, since it does not tie into CreateModel() and thus does not allow you to post whatever and trick the model-binder into creating just any object.

It also works only when the declared property-type is an abstract type, e.g. an abstract class or an interface.

On a related note, it occurs to me that other implementations I've seen here that override CreateModel() probably will only work when you're posting entirely new objects - and will suffer from the same problem I ran into, when the declared property-type is of an abstract type. So you most likely won't be able to edit specific properties of concrete types on existing model objects, but only create new ones.

So in other words, you will probably need to integrate this work-around into your binder to also be able to properly edit objects that were added to the view-model prior to binding... Personally, I feel that's a safer approach, since I control what concrete type gets added - so the controller/action can, indirectly, specify the concrete type that may be bound, by simply populating the property with an empty instance.

I hope this is helpful to others...


Using Darin's method to discriminate your model types via a hidden field in your view, I would recommend that you use a custom RouteHandler to distinguish your model types, and direct each one to a uniquely named action on your controller. For example, if you have two concrete models, Foo and Bar, for your Create action in your controller, make a CreateFoo(Foo model) action and a CreateBar(Bar model) action. Then, make a custom RouteHandler, as follows:

public class MyRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var httpContext = requestContext.HttpContext;
        var modelType = httpContext.Request.Form["ModelType"]; 
        var routeData = requestContext.RouteData;
        if (!String.IsNullOrEmpty(modelType))
        {
            var action = routeData.Values["action"];
            routeData.Values["action"] = action + modelType;
        }
        var handler = new MvcHandler(requestContext);
        return handler; 
    }
}

Then, in Global.asax.cs, change RegisterRoutes() as follows:

public static void RegisterRoutes(RouteCollection routes) 
{ 
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 

    AreaRegistration.RegisterAllAreas(); 

    routes.Add("Default", new Route("{controller}/{action}/{id}", 
        new RouteValueDictionary( 
            new { controller = "Home",  
                  action = "Index",  
                  id = UrlParameter.Optional }), 
        new MyRouteHandler())); 
} 

Then, when a Create request comes in, if a ModelType is defined in the returned form, the RouteHandler will append the ModelType to the action name, allowing a unique action to be defined for each concrete model.

ReferenceURL : https://stackoverflow.com/questions/7222533/polymorphic-model-binding

반응형