2016-10-08 11 views
1

У меня есть следующие классы в моем ASP.NET приложения MVC5:ASP.NET MVC Cast ViewModel в общий тип интерфейса в фильтре действия

public abstract class BaseItem 
{ } 

public class DerivedItem1 : BaseItem 
{ } 

public class DerivedItem2 : BaseItem 
{ } 

public interface IItemViewModel<T> 
    where T : BaseItem 
{ 
    T Item { get; set; } 
} 

public class ItemViewModel<T> : IItemViewModel<T> 
    where T : BaseItem 
{ 
    public T Item { get; set; } 
} 

Мои контроллеры похожи на эти:

[AutoAssignItem] 
public class DerivedItem1Controller : ItemControllerBase<DerivedItem1> 
{ 
    public ActionResult Index() 
    { 
     var model = new ItemViewModel<DerivedItem1>(); 

     // I'd like to avoid setting the Item property here 
     // and instead delegate that task to my filter 
     // itemService.GetCurrentItems returns an instance 
     // of type DerivedItem1. 
     // model.Item = itemService.GetCurrentItem(); 

     return View(model); 
    } 
} 

[AutoAssignItem] 
public class DerivedItem2Controller : ItemControllerBase<DerivedItem2> 
{ 
    public ActionResult Index() 
    { 
     var model = new ItemViewModel<DerivedItem2>(); 

     // I'd like to avoid setting the Item property here 
     // and instead delegate that task to my filter 
     // itemService.GetCurrentItems returns an instance 
     // of type DerivedItem2. 
     // model.Item = itemService.GetCurrentItem(); 

     return View(model); 
    } 
} 

I есть фильтр действий AutoAssignItem, где я хотел бы установить свойство Item на моей модели представления, которая может быть типа ItemViewModel <DerivedItem1> или ItemViewModel <DerivedItem2>:

public class AutoAssignItem : ActionFilterAttribute, IActionFilter 
{ 
    public void OnResultExecuting(ResultExecutingContext filterContext) 
    { 
     var viewModel = filterContext.Controller.ViewData.Model; 

     // The viewModel type here could either be 
     // ItemViewModel<DerivedItem1> or ItemViewModel<DerivedItem2> 
     // So I try passing in BaseItem as the type parameter and cast 
     var model = viewModel as IItemViewModel<BaseItem>; 

     // But model is always null :(
     if (model == null) 
     { 
      return; 
     } 

     // Here I can also try and get the implemented interface type 
     // var interfaceType = viewModel.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IItemViewModel<>)).SingleOrDefault(); 
     // And try converting it but this would require the viewModel 
     // to implement the IConvertible interface which I want to avoid 
     // var model = Convert.ChangeType(viewModel, interfaceType); 

     // If model is not null, then set the Item property 
     // through a service based on contextual information 
     // Here itemService.GetCurrentItem() would return an Item 
     // with the correct type such as DerivedItem1 if the action 
     // on the DerivedItem1Controller had run 
     model.Item = itemService.GetCurrentItem(); 
    } 
} 

Обратите внимание, что BaseItem будет иметь несколько производных классов, а не только два, как в приведенном выше примере. Мой вопрос заключается в том, как мне настроить viewmodel таким образом, чтобы я мог получить доступ и установить свойство Item?

Если я делаю T в IItemViewModel , чтобы быть ковариантным, тогда я не могу установить свойство Item в фильтре действия, поскольку он будет только для getter-only.

Как в стороне, я пытаюсь реплицировать общий контроллер и структуру viewmodel, как это обычно реализуется при использовании Epyser CMS API. Разница заключается в том, что в CMS все это касается страниц. Таким образом, контроллер будет выглядеть так:

public class HomePageController : PageControllerBase<HomePage> 
{ 
    public ActionResult Index(HomePage currentPage) 
    { 
     var model = new PageViewModel<HomePage>(); 
     model.CurrentPage = currentPage; 

     return View(model); 
    } 
} 

Любая помощь приветствуется.

+0

Чего вы надеетесь достичь? Мне просто интересно. – Win

+0

Win - Я добавил комментарии, чтобы уточнить мои намерения. – Khan

+0

Если вы просто пытались * получить * 'model.Item' в фильтре, я бы предложил разделить геттер на ковариантный интерфейс, но концепция дисперсии существует именно для защиты от логически недействительной вещи, которую вы пытаетесь сделать - присвойте 'BaseItem' (предположительно возвращаемый тип' GetCurrentItem') переменной переменной T: BaseItem. Трудно точно узнать, чего вы пытаетесь достичь здесь, но похоже, что вы ищете обходной путь, когда вам нужен совсем другой подход. –

ответ

0

В качестве обходного пути я решил установить свойство Item модели представления, переопределив метод базового контроллера OnResultExecuting, а не в фильтр действий. Таким образом, я смогу получить доступ к параметру типа модели просмотра и сделать соответственно :)

public abstract ItemControllerBase<T> : Controller 
    where T : BaseItem 
{ 
    protected override void OnResultExecuting(ResultExecutingContext filterContext) 
    { 
     base.OnResultExecuting(filterContext); 

     var viewModel = filterContext.Controller.ViewData.Model; 

     var model = viewModel as IItemViewModel<T>; 
     if (model == null) 
     { 
      return; 
     } 

     model.Item = itemService.GetCurrentItem(); 
    } 
} 
1

Похоже, вы пытаетесь создать фильтр действий, который будет применять одно и то же правило правила элемента к нескольким действиям, чтобы вам не приходилось повторять одну и ту же логику определения предметов в каждом действии.

Я использовал весь код, который вы предоставили в новом проекте MVC 5, и внесла следующие изменения, чтобы я мог видеть, что происходит на странице, созданной моим действием под названием «Тест».

public abstract class BaseItem 
{ 
    public String Type { get; set; } 
    public String Note { get; set; } 

    public abstract void Alter(); 
} 

public class DerivedItem1 : BaseItem 
{ 
    public DerivedItem1() 
    { 
     Type = "DerivedItem1"; 
    } 

    public override void Alter() 
    { 
     Note = "Altered by the code specific to DerivedItem1"; 
    } 
} 

public class DerivedItem2 : BaseItem 
{ 
    public DerivedItem2() 
    { 
     Type = "DerivedItem2"; 
    } 

    public override void Alter() 
    { 
     Note = "Altered by the code specific to DerivedItem2"; 
    } 
} 

public class MyActionFilter : ActionFilterAttribute, IActionFilter 
{ 
    public override void OnResultExecuting(ResultExecutingContext filterContext) 
    { 
     var viewModel = filterContext.Controller.ViewData.Model; 

     var model = viewModel as IItemViewModel<BaseItem>; 
     // model is always null :(
     if (model == null) 
     { 
      return; 
     } 
     model.Item = new DerivedItem2(); // Set the Item property here; 
    } 
} 

И в главном контроллере, я создал следующий тест действия:

[MyActionFilter] 
public ActionResult Test() 
{ 
    var vm = new ItemViewModel<BaseItem>(); 
    vm.Item = new DerivedItem1(); 
    return View(vm); 
} 

, а затем в представлении для действия Test:

@model ActionFilterTest.ViewModels.ItemViewModel<ActionFilterTest.Models.BaseItem> 

<h4>BaseItem</h4> 
<hr /> 
<label for="item">Model.Item.Type: </label> 
<div name="item">@Model.Item.Type</div> 

@{ Model.Item.Alter(); } 

<label for="note">Model.Item.Note:</label>" 
<div name="note">@Model.Item.Note</div> 

И когда я просматриваю к дому/Test, он показывает, что Model.Item.Type - DerivedItem2, а не DerivedItem1, и что Note был назначен переопределенным методом в правильном, der ived класс, хотя Alter() был вызван на объект, который был объявлен как тип ItemViewModel<BaseItem>, что и я ожидал. Разве это не то, что вы хотите?

Update: У меня есть ощущение, что вы не собираетесь рассматривать предложение ниже, чтобы быть очень гибким решением, так как вам нужно сделать if ветви для каждого производного типа. Вы можете смоделировать какой-то динамический бросок через отражение. Я действительно не хочу туда ехать. Кажется, что в сообщениях, которые я прочитал, есть консенсус, что динамическое кастинг несколько бессмысленно и даже не рекомендуется, поскольку он пытается обойти механизм проверки типа времени компиляции, который предназначен для предотвращения ошибок во время выполнения и поддержания целостности.Следующие изменения включают новые детали, которые вы указали в своем последнем комментарии, и правильно работаете в моем проекте.

public override void OnResultExecuting(ResultExecutingContext filterContext) 
{ 
    var viewModel = filterContext.Controller.ViewData.Model; 

    var model = viewModel as IItemViewModel<BaseItem>; 

    if (viewModel.GetType() == typeof(ItemViewModel<DerivedItem1>)) 
    { 
     ((ItemViewModel<DerivedItem1>)viewModel).Item = new DerivedItem1(); 
    } 
    else if (viewModel.GetType() == typeof(ItemViewModel<DerivedItem2>)) 
    { 
     ((ItemViewModel<DerivedItem2>)viewModel).Item = new DerivedItem2(); 
    } 
    else 
    { 
     throw new InvalidCastException("Unsupported cast from type: " + viewModel.GetType().FullName); 
    } 
} 

Затем я удалил Test действие с HomeController и добавил следующее:

[MyActionFilter] 
public ActionResult Test1() 
{ 
    var vm = new ItemViewModel<DerivedItem1>(); 
    return View(vm); 
} 

[MyActionFilter] 
public ActionResult Test2() 
{ 
    var vm = new ItemViewModel<DerivedItem2>(); 
    return View(vm); 
} 

Со следующими видами:

@model ActionFilterTest.ViewModels.ItemViewModel<ActionFilterTest.Models.DerivedItem1> 

<label for="type">Model.Item.Type:</label> 
<div name="type">@Model.Item.Type</div> 

@{ Model.Item.Alter(); } 

<label for="note">Model.Item.Note:</label>" 
<div name="note">@Model.Item.Note</div> 

и:

@model ActionFilterTest.ViewModels.ItemViewModel<ActionFilterTest.Models.DerivedItem2> 

<label for="type">Model.Item.Type:</label> 
<div name="type">@Model.Item.Type</div> 

@{ Model.Item.Alter(); } 

<label for="note">Model.Item.Note:</label>" 
<div name="note">@Model.Item.Note</div> 
+0

Кент Вейгель - Ну, не совсем. Вы правы в том, что я хочу применить фильтр действий, чтобы избежать повторения установки свойства Item в модели представления. Тем не менее параметр type для viewmodel всегда будет установлен в производном классе (и ** not ** BaseItem) в моем действии контроллера, например: var vm = new ItemViewModel (); 'Я обновил мой вопрос с большим количеством комментариев. – Khan

+0

Я отредактировал свой ответ, чтобы показать, что полиморфизм работает по назначению. Когда вы присваиваете экземпляр более производного класса переменной, объявленной как менее производный класс, когда вы вызываете методы переменной, фактический метод называется именем производного типа. Вы должны будете объяснить мне, почему это не сработает для вас. Если вы указали переменную более производного типа и попытались присвоить ей значение другого одинаково производного типа, например, «ItemViewModel » и «ItemViewModel », соответственно, я не думаю, что вы бы много удачи. –

+0

И поскольку вы не можете создать экземпляр абстрактного класса, вы автоматически защищены от случайного его выполнения и намерения назначить 'vm' или в вашем случае' model', подразумеваются только производные типы. –