如何将 IViewLocationExpander 与 Razor Pages 一起使用以呈现设备特定页面
How do I use IViewLocationExtender with Razor Pages to render device specific pages
目前我们正在构建一个 Web 应用程序,首先是桌面,它需要针对特定页面的特定于设备的 Razor 页面。这些页面与它们的桌面版本确实不同,在这里使用响应性是没有意义的。
我们已经尝试实现我们自己的 IViewLocationExpander 并且还尝试使用 MvcDeviceDetector 库(基本上是做同样的事情)。设备类型的检测没有问题,但由于某种原因,设备特定页面未被拾取并且不断回退到默认值 Index.cshtml。
(编辑:我们正在考虑实现基于 IPageConvention、IPageApplicationModelProvider 或其他东西的东西......;-))
Index.mobile.cshtml
Index.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 仿真时看到 Index.mobile.cshtml 被拾取,但那没有发生。
编辑 注:
- 我们结合使用了 Razor Views/MVC(较旧的部分)和 Razor Pages(较新的部分)。
- 也不是每个页面都有移动实现。这就是 IViewLocationExpander 解决方案如此出色的原因。
编辑 2
我认为解决方案与您实现特定于文化的 Razor Pages 的方式相同(我们也不知道 ;-))。基本 MVC 支持 Index.en-US.cshtml
下面的最终解决方案
如果这是一个 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>();
我终于找到了基于约定的方法。我已经实现了 IViewLocationExpander 来处理基本 Razor 视图(包括布局)的设备处理,并且我已经实现了 IPageRouteModelConvention + IActionConstraint 来处理 Razor 页面的设备。
注意: 此解决方案似乎只适用于 ASP.NET Core 2.2 及更高版本。出于某种原因 2.1.x 及以下内容在添加约束后(可能已修复)清除约束(在析构函数中使用断点进行测试)。
现在我可以在 MVC 和 Razor Pages 中拥有 /Index.mobile.cshtml /Index.desktop.cshtml 等。
注意:此解决方案还可用于实现 language/culture 特定的 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()));
});
目前我们正在构建一个 Web 应用程序,首先是桌面,它需要针对特定页面的特定于设备的 Razor 页面。这些页面与它们的桌面版本确实不同,在这里使用响应性是没有意义的。
我们已经尝试实现我们自己的 IViewLocationExpander 并且还尝试使用 MvcDeviceDetector 库(基本上是做同样的事情)。设备类型的检测没有问题,但由于某种原因,设备特定页面未被拾取并且不断回退到默认值 Index.cshtml。 (编辑:我们正在考虑实现基于 IPageConvention、IPageApplicationModelProvider 或其他东西的东西......;-))
Index.mobile.cshtml Index.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 仿真时看到 Index.mobile.cshtml 被拾取,但那没有发生。
编辑 注:
- 我们结合使用了 Razor Views/MVC(较旧的部分)和 Razor Pages(较新的部分)。
- 也不是每个页面都有移动实现。这就是 IViewLocationExpander 解决方案如此出色的原因。
编辑 2 我认为解决方案与您实现特定于文化的 Razor Pages 的方式相同(我们也不知道 ;-))。基本 MVC 支持 Index.en-US.cshtml
下面的最终解决方案
如果这是一个 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>();
我终于找到了基于约定的方法。我已经实现了 IViewLocationExpander 来处理基本 Razor 视图(包括布局)的设备处理,并且我已经实现了 IPageRouteModelConvention + IActionConstraint 来处理 Razor 页面的设备。
注意: 此解决方案似乎只适用于 ASP.NET Core 2.2 及更高版本。出于某种原因 2.1.x 及以下内容在添加约束后(可能已修复)清除约束(在析构函数中使用断点进行测试)。
现在我可以在 MVC 和 Razor Pages 中拥有 /Index.mobile.cshtml /Index.desktop.cshtml 等。
注意:此解决方案还可用于实现 language/culture 特定的 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()));
});