Asp.net MVC 视图和布局的模型

5
我一直在寻找一种处理Asp.net MVC网站模型的好方法,当所有页面具有共同属性时。这些属性将显示在布局(母版页)中。我使用一个“BaseModel”类来保存这些属性,并且我的布局使用此BaseModel作为其模型。
每个其他模型都继承自该BaseModel,并且每个模型都有相对于其表示的视图的特定属性。正如您可能已经猜到的那样,我的模型实际上是视图模型,即使这在这里并不太相关。
我尝试了不同的方法来初始化BaseModel值:
1. 在每个视图中手动设置 2. 拥有一个基本控制器,它具有一个Initialize虚拟方法来执行它(因此特定控制器可以实现特定的公共行为) 3. 有一个基本控制器,覆盖OnActionExecuting以调用Initialize方法 4. 使用帮助程序类在控制器之外进行操作 5. 使用模型工厂
但是,这些方法都没有真正吸引我:
  1. 对我来说似乎显而易见,但DRY就足以证明这一点了(实际上我从未尝试过那个解决方案,我只是为了能够在最后一点上循环而将其放置在那里)。
  2. 我不喜欢那个方法,因为它意味着每当添加一个新的控制器时,你需要知道它必须从BaseController继承并且你需要调用Initialize方法,更不用说如果你的控制器已经覆盖了基本控制器,仍然需要调用基本控制器以保持值。
  3. 见下一点
  4. 第3点和第4点是同一主题的变体,但这并不能真正解决第二种解决方案的问题。
  5. 到目前为止,这是我的最爱,但现在我必须传递更多的变量来设置这些值。我喜欢它的依赖反转。但是,如果我想要提供来自会话的值,例如,我需要显式地传递它们,那么我又回到了起点,因为我必须手动提供它们(无论是引用还是通过任何类型的接口)。

当然,(几乎)所有这些解决方案都可以工作,但我正在寻找更好的方法来解决这个问题。

在输入这个问题的同时,我发现可能有一条新的路线——构建者模式,但是实现也可能很快变成一个负担,因为我们可能会有几十个视图和控制器。

我很乐意接受任何严肃的推荐/提示/建议/模式/建议!

更新

感谢@EBarr,我想出了另一个解决方案,使用ActionFilterAttribute(不是生产代码,在5分钟内完成):

public class ModelAttribute : ActionFilterAttribute
{
    public Type ModelType { get; private set; }

    public ModelAttribute(string typeName) : this(Type.GetType(typeName)) { }

    public ModelAttribute(Type modelType)
    {
        if(modelType == null) { throw new ArgumentNullException("modelType"); }

        ModelType = modelType;
        if (!typeof(BaseModel).IsAssignableFrom(ModelType))
        {
            throw new ArgumentException("model type should inherit BaseModel");
        }
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var model = ModelFactory.GetModel(ModelType);

        var foo = filterContext.RequestContext.HttpContext.Session["foo"] as Foo;

        model.Foo = foo;
        model.Bar = somevalue;

        filterContext.Controller.TempData["model"] = model;
    } 
}

调用它非常简单:

[Model(typeof(HomeModel))]
public ActionResult Index()
{
    var homeModel = TempData["model"] as HomeModel;

    // Add View Specific stuff

    return View(homeModel);
}

它让我拥有了最好的每个世界。唯一的缺点是找到一个适当的方法来将模型传回操作。

这里使用TempData对象完成,但我也考虑更新在ActionParameters中找到的模型。

我仍然接受任何关于此的严肃建议/提示/建议/模式或之前提到的问题。

2个回答

2
我经历了和你差不多的过程,深入学习MVC。你说得对,这些解决方案都不是很好。
最终,我使用了一系列基础模型。由于各种原因,我有几种不同类型的基础模型,但是逻辑应该适用于单个基础类型。我的大多数视图模型都继承自其中一个基础模型。然后,根据需要/时间,在ActionExecuting或OnActionExecuted中填充模型的基本部分。
以下是我的代码片段,应该可以清楚地说明这个过程:
if (filterContext.ActionParameters.ContainsKey("model")) {
   var tempModel = (System.Object)filterContext.ActionParameters["model"];

   if (typeof(BaseModel_SuperLight).IsAssignableFrom(tempModel.GetType())) {
       //do stuff required by light weight model
   }

   if (typeof(BaseModel_RegularWeight).IsAssignableFrom(tempModel.GetType())) {
      //do more costly stuff for regular weight model here
   }
}

最终我的模式并不太令人满意。然而,它是实用的、灵活的,并且易于实现不同级别的继承。我还能够注入控制器执行前或执行后的操作,这在我的情况下非常重要。希望这能帮到你。

感谢您的反馈。虽然它并不完全符合我所寻找的(您的解决方案是3.的不同实现),但它确实给了我一个想法。我刚刚尝试使用自定义ActionFilterAttribute,似乎可以解决问题。我仍然需要找到一种“干净”的方法将模型返回给操作(现在我正在使用TempData)。我会更新问题以反映这一点。 - Sebastien F.
是的,这是 #3 的一个版本。我考虑过属性,但那需要装饰每个控制器/操作(取决于需要)。将其保留在 OnActionExecutingOnActionExecuted 中可以使我的代码位于单个基本控制器中。在任何较大的 MVC 项目中,您都将拥有一个基本控制器,因此我并不介意。它还允许我根据模型类型运行逻辑,而不是将逻辑与执行的操作绑定在一起。 - EBarr
回复:获取模型:在您的操作筛选器属性中,您最终将实现相同的事件(执行和已执行),但是您可以从filterContext中获取模型,无需将其返回,只需找到它并填写所需内容即可。 - EBarr

1
EBarr给我的建议是使用动作过滤器的想法实际上是可行的,但最终感觉不太对,因为没有干净的方法来检索模型,而不是通过视图包或httpcontext项目等方式传递。同时,这也强制将每个操作与其模型装饰起来。它还使得回发更难处理。我仍然相信这个解决方案有其优点,在某些特定场景下可能很有用。
于是我又回到了起点,开始更深入地研究这个问题。我得出了以下结论。首先,问题有两个方面:
1. 初始化视图数据 2. 渲染数据
在寻找更多想法的过程中,我意识到我没有从正确的角度看待问题。我从“控制器”角度看待问题,而模型的最终客户是视图。我还想起布局/主页面不是一个视图,不应该有一个与之关联的模型。这个想法让我走上了一条对我来说正确的道路。因为这意味着布局的每个“动态”部分都应该在外部处理。当然,由于它们的灵活性,部分似乎是最合适的选择。
在我制作的测试解决方案中,我有(仅)4个不同的部分,有些是强制性的,有些则不是。部分的问题在于,您需要在每个页面上添加它们,这可能很快成为更新/修改的痛点。为了解决这个问题,我尝试了这个方法:
public interface IViewModel
{
    KeyValuePair<string, PartialViewData>[] Sections { get; }
}

public class PartialViewData
{
    public string PartialViewName { get; set; }
    public object PartialViewModel { get; set; }
    public ViewDataDictionary ViewData { get; set; }
}   

例如,我的视图模型是这样的:
public class HomeViewModel : IViewModel
{
    public Article[] Articles { get; set; }             // Article is just a dummy class 
    public string QuickContactMessage { get; set; }     // just here to try things

    public HomeViewModel() { Articles = new Article[0]; }

    private Dictionary<string, PartialViewData> _Sections = new Dictionary<string, PartialViewData>();
    public KeyValuePair<string, PartialViewData>[] Sections
    {
        get { return _Sections.ToArray(); }
        set { _Sections = value.ToDictionary(item => item.Key, item => item.Value); }
    }
}

这个在操作中被初始化:

public ActionResult Index()
{
    var hvm = ModelFactory.Get<HomeViewModel>(); // Does not much, basicaly a new HomeViewModel();

    hvm.Sections = LayoutHelper.GetCommonSections().ToArray(); // more on this just after
    hvm.Articles = ArticlesProvider.GetArticles(); // ArticlesProvider could support DI

    return View(hvm);
}

LayoutHelper是控制器上的一个属性(如果需要,可以进行DI注入):

public class DefaultLayoutHelper
{
    private Controller Controller;
    public DefaultLayoutHelper(Controller controller) { Controller = controller; }

    public Dictionary<string, PartialViewData> GetCommonSections(QuickContactModel quickContactModel = null)
    {
        var sections = new Dictionary<string, PartialViewData>();
        // those calls were made in methods in the solution, I removed it to reduce the length of the answer
        sections.Add("header",
                     Controller.UserLoggedIn() // simple extension that check if there is a user logged in
                     ? new PartialViewData { PartialViewName = "HeaderLoggedIn", PartialViewModel = new HeaderLoggedInViewModel { Username = "Bishop" } } 
                     : new PartialViewData { PartialViewName = "HeaderNotLoggedIn", PartialViewModel = new HeaderLoggedOutViewModel() });
        sections.Add("quotes", new PartialViewData { PartialViewName = "Quotes" });
        sections.Add("quickcontact", new PartialViewData { PartialViewName = "QuickContactForm", PartialViewModel = model ?? new QuickContactModel() });
        return sections;
    }
}

在视图文件(.cshtml)中:

@section       quotes { @{ Html.RenderPartial(Model.Sections.FirstOrDefault(s => s.Key == "quotes").Value); } }
@section        login { @{ Html.RenderPartial(Model.Sections.FirstOrDefault(s => s.Key == "header").Value); } }
@section       footer { @{ Html.RenderPartial(Model.Sections.FirstOrDefault(s => s.Key == "footer").Value); } }

实际解决方案有更多的代码,我试图简化以便在这里得到想法。它仍然有点粗糙,需要打磨/错误处理,但是通过这样做,我可以在我的操作中定义部分将是什么,它们将使用哪个模型等等。它可以很容易地进行测试,并且设置DI不应该成为问题。
我仍然必须在每个视图中复制@section行,这似乎有点痛苦(特别是因为我们不能将部分放在部分视图中)。
我正在研究templated razor delegates,看看它是否可以替换部分。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接