【问题标题】:How do I use IViewLocationExtender with Razor Pages to render device specific pages如何将 IViewLocationExtender 与 Razor 页面一起使用来呈现设备特定页面
【发布时间】:2019-07-15 15:17:35
【问题描述】:

目前我们正在构建一个 web 应用程序,首先是桌面,它需要特定页面的设备特定 Razor 页面。这些页面与它们的桌面版本确实不同,在这里使用响应性是没有意义的。

我们已经尝试实现我们自己的 IViewLocationExpander 并且还尝试使用 MvcDeviceDetector 库(基本上是这样做的)。设备类型的检测没有问题,但由于某种原因,设备特定页面没有被拾取,并且不断回退到默认的 Index.cshtml。 (编辑:我们正在考虑基于 IPageConvention、IPageApplicationModelProvider 或其他东西实现一些东西...... ;-)

Index.mobile.cshtml 索引.cshtml

我们使用 MvcDeviceDetector 的示例添加了以下代码:

public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
    builder.Services.AddDeviceSwitcher<UrlSwitcher>(
    o => { },
    d => {
            d.Format = DeviceLocationExpanderFormat.Suffix;
            d.MobileCode = "mobile";
        d.TabletCode = "tablet";
    }
    );

    return builder;
}

并且正在添加一些路线映射

routes.MapDeviceSwitcher();

我们希望在 Chrome 中选择 Phone Emulation 时会看到 Index.mobile.cshtml,但没有发生。

编辑注意:

  • 我们正在使用 Razor 视图/MVC(旧部分)和 Razor 页面(较新部分)的组合。
  • 也不是每个页面都有移动实现。这就是 IViewLocationExpander 解决方案如此出色的原因。

编辑 2 我认为解决方案与您实施特定于文化的 Razor 页面的方式相同(我们也不知道;-))。基本 MVC 支持 Index.en-US.cshtml

下面的最终解决方案

【问题讨论】:

  • 您能否说明您使用的是 Razor Pages 还是 MVC?
  • 我们实际上同时使用了两者。该站点的新部分使用 Razor Pages,而旧部分使用“old skool” Razor Views。并且很可能在未来的部分中将开始使用 Blazor。原因:因为你可以而且 MVC 允许你这样做 ;-) 但是这个问题是关于 Razor 页面的,MVC 路由很好。

标签: asp.net-core razor-pages device-detection


【解决方案1】:

如果这是一个 Razor Pages 应用程序(相对于 MVC 应用程序),我认为IViewLocationExpander 接口对您没有多大用处。据我所知,它只适用于部分页面,而不适用于可路由页面(即带有@page 指令的页面)。

您可以做的是使用中间件来确定请求是否来自移动设备,然后将要执行的文件更改为以.mobile结尾的文件。这是一个非常粗略且现成的实现:

public class MobileDetectionMiddleware
{
    private readonly RequestDelegate _next;

    public async Task Invoke(HttpContext context)
    {
        if(context.Request.IsFromAMobileDevice())
        {
            context.Request.Path = $"{context.Request.Path}.mobile";
        }
        await _next.Invoke(context);
    }
}

由您决定如何实现IsFromAMobileDevice 方法来确定用户代理的性质。没有什么可以阻止您使用可以为您可靠地进行检查的第三方库。此外,您可能只想在某些条件下更改路径 - 例如请求页面的设备特定版本。

尽早在您的Configure 方法中注册:

app.UseMiddleware<MobileDetectionMiddleware>();

【讨论】:

  • 我想知道这是否可行。由于此解决方案建议每个页面都应该有一个移动版本,但事实并非如此。只有特定页面(我应该在问题中写下)
  • 正如我在回答中所说,您可能希望对更改路径的代码应用条件 - 仅在请求的页面具有专用移动版本的情况下。它是一个基本实现,仅用于说明概念。
【解决方案2】:

我终于找到了基于约定的方法。我已经实现了 IViewLocationExpander 来处理基本 Razor 视图(包括布局)的设备处理,并且我已经实现了 IPageRouteModelConvention + IActionConstraint 来处理 Razor 页面的设备。

注意: 这个解决方案似乎只适用于 ASP.NET Core 2.2 及更高版本。出于某种原因,2.1.x 及以下版本在添加约束(可能已修复)后清除约束(使用析构函数中的断点进行测试)。

现在我可以在 MVC 和 Razor 页面中拥有 /Index.mobile.cshtml /Index.desktop.cshtml 等。

注意:此解决方案还可用于实现特定于语言/文化的 Razor 页面(例如 /Index.en-US.cshtml /Index.nl-NL.cshtml)

public class PageDeviceConvention : IPageRouteModelConvention
{
    private readonly IDeviceResolver _deviceResolver;

    public PageDeviceConvention(IDeviceResolver deviceResolver)
    {
        _deviceResolver = deviceResolver;
    }

    public void Apply(PageRouteModel model)
    {
        var path = model.ViewEnginePath; // contains /Index.mobile
        var lastSeparator = path.LastIndexOf('/');
        var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);

        if (lastDot != -1)
        {
            var name = path.Substring(lastDot + 1);

            if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
            {
                var constraint = new DeviceConstraint(deviceType, _deviceResolver);

                 for (var i = model.Selectors.Count - 1; i >= 0; --i)
                {
                    var selector = model.Selectors[i];
                    selector.ActionConstraints.Add(constraint);

                    var template = selector.AttributeRouteModel.Template;
                    var tplLastSeparator = template.LastIndexOf('/');
                    var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));

                    template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
                    selector.AttributeRouteModel.Template = template;

                    var fileName = template.Substring(tplLastSeparator + 1);
                    if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
                    {
                        selector.AttributeRouteModel.SuppressLinkGeneration = true;
                        template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
                        model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
                    }
                }
            }
        }
    }

    protected class DeviceConstraint : IActionConstraint
    {
        private readonly DeviceType _deviceType;
        private readonly IDeviceResolver _deviceResolver;

        public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
        {
            _deviceType = deviceType;
            _deviceResolver = deviceResolver;
        }

        public int Order => 0;

        public bool Accept(ActionConstraintContext context)
        {
            return _deviceResolver.GetDeviceType() == _deviceType;
        }
    }
}

public class DeviceViewLocationExpander : IViewLocationExpander
{
    private readonly IDeviceResolver _deviceResolver;
    private const string ValueKey = "DeviceType";

    public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
    {
        _deviceResolver = deviceResolver;
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {
        var deviceType = _deviceResolver.GetDeviceType();

        if (deviceType != DeviceType.Other)
            context.Values[ValueKey] = deviceType.ToString();
    }

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        var deviceType = context.Values[ValueKey];
        if (!string.IsNullOrEmpty(deviceType))
        {
            return ExpandHierarchy();
        }

        return viewLocations;

        IEnumerable<string> ExpandHierarchy()
        {
            var replacement = $"{{0}}.{deviceType}";

            foreach (var location in viewLocations)
            {
                if (location.Contains("{0}"))
                    yield return location.Replace("{0}", replacement);

                yield return location;
            }
        }
    }
}

public interface IDeviceResolver
{
    DeviceType GetDeviceType();
}

public class DefaultDeviceResolver : IDeviceResolver
{
    public DeviceType GetDeviceType() => DeviceType.Mobile;
}

public enum DeviceType
{
    Other,
    Mobile,
    Tablet,
    Normal
}

启动

services.AddMvc(o => { })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
            .AddRazorOptions(o =>
            {
                o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
            })
            .AddRazorPagesOptions(o =>
            {
                o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
            });

【讨论】:

    猜你喜欢
    • 2013-05-18
    • 2018-09-29
    • 1970-01-01
    • 2022-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多