如何根据设备类型更改ASP.NET MVC视图?

27

我正在阅读一些ASP.NET MVC的内容,我将把工作中的WebForms迁移到MVC。在这个过程中,我希望会有一个功能请求,即如果用户来自移动设备,则返回简化的视图。

我还不确定最佳实现该逻辑的位置。我相信有更好的方法,而不是在每个返回视图的操作中添加一个针对Browser.IsMobileDevice的if/else语句。我可以采用哪些选项来实现这一点?

5个回答

21

更新: 此解决方案存在一个微妙的错误。MVC框架将调用FindView/FindPartialView两次:第一次使用useCache=true,如果不返回结果,则第二次使用useCache=false。由于所有类型视图只有一个缓存,如果桌面浏览器先到达,则移动用户可能会看到桌面视图。

对于那些有兴趣使用自定义视图引擎来解决这个问题的人,Scott Hanselman在这里更新了他的解决方案:

http://www.hanselman.com/blog/ABetterASPNETMVCMobileDeviceCapabilitiesViewEngine.aspx

(对于我的回答劫持表示歉意,我只是不想让其他人再经历这样的问题!)

编辑者:roufamatic (2010-11-17)


首先,你需要引入Mobile Device Browser File到你的项目中。使用此文件,您可以针对任何想要支持的设备,而无需知道这些设备发送的特定信息。这个文件已经为你完成了这项工作。然后,你使用Request.Browser属性来定制你想要返回的视图。

接下来,考虑一下你想要如何组织Views文件夹中的视图。我喜欢将桌面版本保留在根目录下,然后有一个Mobile文件夹。例如,Home视图文件夹看起来像这样:

  • Home
    • Mobile
      • iPhone
        • Index.aspx
      • BlackBerry
        • Index.aspx
    • Index.aspx

我不同意@Mehrdad关于使用自定义视图引擎的观点。视图引擎具有多个用途之一是为控制器查找视图。你可以通过重写FindView方法来实现这一点。在这个方法中,你可以检查应该去哪里找视图。确定使用您为组织视图而提出的策略之后,您可以返回该设备的视图。

public class CustomViewEngine : WebFormViewEngine
{
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        // Logic for finding views in your project using your strategy for organizing your views under the Views folder.
        ViewEngineResult result = null;
        var request = controllerContext.HttpContext.Request;

        // iPhone Detection
        if (request.UserAgent.IndexOf("iPhone",
   StringComparison.OrdinalIgnoreCase) > 0)
        {
            result = base.FindView(controllerContext, "Mobile/iPhone/" + viewName, masterName, useCache);
        }

        // Blackberry Detection
        if (request.UserAgent.IndexOf("BlackBerry",
   StringComparison.OrdinalIgnoreCase) > 0)
        {
            result = base.FindView(controllerContext, "Mobile/BlackBerry/" + viewName, masterName, useCache);
        }

        // Default Mobile
        if (request.Browser.IsMobileDevice)
        {
            result = base.FindView(controllerContext, "Mobile/" + viewName, masterName, useCache);
        }

        // Desktop
        if (result == null || result.View == null)
        {
            result = base.FindView(controllerContext, viewName, masterName, useCache);
        }

        return result;
    }
}

以上代码允许您根据策略设置视图。如果没有找到设备的视图或者没有默认的移动视图,那么回退为桌面视图。

如果你决定把逻辑放在控制器中而不是创建一个视图引擎。最好的方法是创建一个自定义ActionFilterAttribute,并将其装饰在控制器上。然后重写OnActionExecuted方法来确定是哪个设备正在查看您的网站。您可以查看此博客文章了解如何操作。该文章还提供了一些关于这个主题的Mix视频的有用链接。


无法与T4MVC一起使用,因为您假定视图名称只是一个名称而不是路径。也无法在发布模式下工作,因为缓存的原因。 - Carl Hörberg
1
这个解决方案有一个漏洞!事实证明,卡尔是对的,缓存确实是一个问题,但不是他所描述的那种方式。问题在于 fall-through 的逻辑。如果 fall-through 的结果被缓存,但设备特定视图没有被缓存,那么即使设备特定视图可用,浏览器也会接收到 fall-through。 - roufamatic
解决方案并不是像Carl在下面所做的那样禁用缓存,而是对于每种检测调用两次base.FindView -- 一次使用useCache = true,如果这不能返回视图,则再一次使用useCache = false。只有在这样做之后,才应该允许默认情况下的跌落。 - roufamatic
1
Hanselman回应。http://www.hanselman.com/blog/ABetterASPNETMVCMobileDeviceCapabilitiesViewEngine.aspx - roufamatic
我们在NerdDinner中添加了一个新的MobileCapableViewEngine,修复了这个错误。http://nerddinner.codeplex.com/SourceControl/changeset/view/69349#1511642 - Scott Hanselman
显示剩余3条评论

2
在模型-视图-控制器模式中,是控制器选择视图,因此添加一个if语句并返回适当的视图并不是很糟糕。您可以将if语句封装在一个方法中并调用它:
return AdaptedView(Browser.IsMobileDevice, "MyView.aspx", model);

另一种方法是创建一个视图引擎,根据是否为移动设备动态执行视图。我不喜欢这种方法,因为我认为控制器应该负责。例如,如果您在iPhone上浏览,可能希望看到完整的桌面版本。在前一种方法中,您将传递适当的布尔标志,但在后一种方法中,事情变得更加复杂。


2

这是一个实际可行的版本,可以与T4MVC一起使用,并且在发布模式下也可以工作(其中启用了视图缓存)。它还可以处理用户控件和绝对/相对URL。它需要移动设备浏览器文件

public class MobileCapableWebFormViewEngine : WebFormViewEngine
{

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        if (viewPath.EndsWith(".ascx"))
            masterPath = "";
        return base.CreateView(controllerContext, viewPath, masterPath);
    }
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        useCache = false;
        ViewEngineResult result = null;
        var request = controllerContext.HttpContext.Request;

        if (request.Browser.IsMobileDevice || request["mobile"] != null || request.Url.Host.StartsWith("m."))
        {
            var mobileViewName = GetMobileViewName(viewName);

            result = base.FindView(controllerContext, mobileViewName, masterName, useCache);
            if (result == null || result.View == null)
            {
                result = base.FindView(controllerContext, viewName, "Mobile", useCache);
            }
        }

        if (result == null || result.View == null)
        {
            result = base.FindView(controllerContext, viewName, masterName, useCache);
        }

        return result;
    }

    private static string GetMobileViewName(string partialViewName)
    {
        var i = partialViewName.LastIndexOf('/');
        return i > 0
                   ? partialViewName.Remove(i) + "/Mobile" + partialViewName.Substring(i)
                   : "Mobile/" + partialViewName;
    }
}

谢谢您的跟进,但我仍然不明白为什么您不信任缓存。据我所知,允许缓存应该可以加速对base.FindView()的调用。这些调用发生在正确的视图被选择之后。通过反射器进行检查可以确认。如果您预计这些视图会更改,我可以理解禁用缓存,但这里并非如此。视图是相同的,我们只是添加了选择它们的逻辑。我将坚持使用缓存。 - roufamatic

2
我认为插入此功能的正确位置是自定义 ViewEngine。但您应该了解 ViewEngineCollection 如何调用 IViewEngine.FindView 方法(在此处了解更多 here)。
Scott Hanselman 建议的更新 solution 不起作用。您可以在 here 中找到我对此方法的示例实现。查看自述文件,了解如何重复不正确的行为。
我建议另一种方法,它检查原始 ViewEngine 是否未找到视图,并且如果 useCache 参数为 true,则使用参数 useCache=false 检查原始 ViewEngine 是否存在视图。
这里放置所有代码过于复杂,但你可以在我的开源游乐场 这里 找到实现建议方法。请查看MobileViewEngine类和单元测试。
一些MobileViewEngine的特点:
  • 正确地使用视图缓存,并使用原始视图引擎缓存。
  • 支持两种方式:MvcContrib T4模板使用的短视图名称和相对视图路径(~/Views/Index)。
  • 解析“Index”视图如下:
    • Mobile/Platform/Index - 如果视图存在并且在支持列表中列出了移动设备平台(IPhone、Android等)。
    • Mobile/Index - 所有其他移动设备的视图。如果该视图不存在,您可以选择回退到桌面视图版本。
    • Index - 适用于桌面视图版本。
  • 您可以通过添加/更改设备规则(请参阅MobileDeviceRulePlatformSpecificRule)来自定义移动视图层次结构(例如Mobile/ Platform/Manufacturer)或自定义移动视图路径解析。

希望这可以帮助您


0

你的核心逻辑应该在控制器中保持一致,只有需要的视图会发生变化,因此控制器是需要 if/else 语句来为每个控制器操作提供正确视图的地方,就像你所说的那样。

另一种方法是将控制器逻辑封装在单独的 dll 中,然后为移动版本设置不同的控制器/路径。如果常规控制器从移动设备接收到请求,则可以将它们重定向到包含所有移动控制器的移动区域,这些控制器使用共享控制器逻辑。这种解决方案还允许您进行针对移动控制器的特定“微调”,而不会影响常规控制器。


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