ASP.在动作过滤器中将视图模型转换为通用接口类型

本文关键字:转换 模型 接口类型 视图 过滤器 ASP | 更新日期: 2023-09-27 18:12:22

我有以下类在我的ASP。NET MVC5应用程序:

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

我有一个AutoAssignItem动作过滤器,我想在我的视图模型上设置Item属性,可以是类型ItemViewModel或ItemViewModel:

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将有几个派生类,而不是像上面的例子那样只有两个。我的问题是我如何转换视图模型,以便我可以访问和设置Item属性?

如果我在IItemViewModel如果是协变的,那么我就不能在动作过滤器中设置Item属性,因为它将是只接收的。

作为旁白,我试图复制通用控制器和视图模型结构,通常是使用Episerver CMS API时实现的。不同之处在于,在CMS中,一切都是关于页面的。所以控制器看起来像:

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

ASP.在动作过滤器中将视图模型转换为通用接口类型

听起来好像您正在尝试创建一个操作过滤器,它将对多个操作应用相同的项设置规则,以便您不必在每个操作中重复相同的项设置逻辑。

我使用了您在一个新的MVC 5项目中提供的所有代码,并进行了以下更改,以便我可以看到由我的名为"Test"的操作创建的页面中发生了什么。"

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;
    }
}
在Home控制器中,我创建了以下Test动作:
[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>

当我浏览到Home/Test时,它显示Model.Item.TypeDerivedItem2,而不是DerivedItem1,并且Note是由正确的派生类中的重写方法分配的,即使Alter()是在被声明为ItemViewModel<BaseItem>类型的对象上调用的,这是我所期望的。这不是你想要的吗?

:我有一种感觉,您不会认为下面的建议是一个非常灵活的解决方案,因为您需要为每个派生类型创建一个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);
    }
}

然后我从HomeController中删除了Test动作,并添加了以下内容:

[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>

作为一种变通方法,我决定通过重写基控制器的OnResultExecuting方法而不是在操作过滤器中设置视图模型的Item属性。这样,我就可以访问视图模型的类型参数并进行相应的类型转换:)

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