【ASP.NET Core】MVC中自定义视图的查找位置

.NET Core 的内容处处可见,刷爆全球各大社区,所以,老周相信各位大伙伴已经看得不少了,故而,老周不考虑一个个知识点地去写,那样会成为年度最大的屁话,何况官方文档也很详尽。老周主要扯一下大伙伴们在入门的时候可能会疑惑的内容。

ASP.NET Core 可以在一个项目中混合使用 Web Pages 和 MVC ,这是老周最希望的,因为这样会变得更灵活。Web Pages 类似于我们过去的 Web 开发方式,以页面为单位,此模型侧重于功能划分。而 MVC 侧重于数据,有什么样的数据模型就有什么样的 Controller,有什么样的 Controller 就会对应什么样的 Action ,而 Action 又会有对应的 UI,即 View。所以说 MVC 是以数据为核心的。

如果两者可以同时使用,那在我的项目中,可能有些内容以功能为重点,而另一些内容是以数据为中心的,这样可以灵活地交替使用,因此,老周向来最喜欢空项目模板,因为空的什么都没有,什么都没有才能做到什么都有。大概,老庄所说的“无”,与佛家所说的“空”,就是这样的。

Web Pages 和 MVC 可以一起用,是因为它们的配置方法是一样的,在 Startup 类中,有两个约定的方法。

ConfigureServices 方法是告诉应用程序我要用到哪些功能,Service 是用来扩展的,你自己也可以编写各种功能,然后添加到 services 集合中就好了。不管是 W  eb Pages 还是 MVC ,都是添加这一行代码

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

估计大家会发现,除了 AddMvc 方法外,还有一个 AddMvcCore 方法,你一定会有疑问,这两个家伙一家吗?于是,你会尝试一下把 AddMvc 换成 AddMvcCore ,然后运行时你会发现找不到视图。

带 Core 结尾的方法,只添加核心的功能,并非 MVC 所需的必备功能,此方法也许更适合 Web API,但即便我们写的是 API 项目,我们也极少用这个方法,所以,在实际开发中,你可以直接无视 AddMvcCore 方法。

那么,这哥儿俩到底有啥不同呢。咱们不妨看看源代码。AddMvcCore 主要添加了以下功能。

            //
            // Options
            //
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcOptionsConfigureCompatibilityOptions>());
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());

            //
            // Action Discovery
            //
            // These are consumed only when creating action descriptors, then they can be deallocated

            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IApplicationModelProvider, DefaultApplicationModelProvider>());
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IApplicationModelProvider, ApiBehaviorApplicationModelProvider>());
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());

            services.TryAddSingleton<IActionDescriptorCollectionProvider, ActionDescriptorCollectionProvider>();

            //
            // Action Selection
            //
            services.TryAddSingleton<IActionSelector, ActionSelector>();
            services.TryAddSingleton<ActionConstraintCache>();

            // Will be cached by the DefaultActionSelector
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());

            //
            // Controller Factory
            //
            // This has a cache, so it needs to be a singleton
            services.TryAddSingleton<IControllerFactory, DefaultControllerFactory>();

            // Will be cached by the DefaultControllerFactory
            services.TryAddTransient<IControllerActivator, DefaultControllerActivator>();

            services.TryAddSingleton<IControllerFactoryProvider, ControllerFactoryProvider>();
            services.TryAddSingleton<IControllerActivatorProvider, ControllerActivatorProvider>();
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IControllerPropertyActivator, DefaultControllerPropertyActivator>());

            //
            // Action Invoker
            //
            // The IActionInvokerFactory is cachable
            services.TryAddSingleton<IActionInvokerFactory, ActionInvokerFactory>();
            services.TryAddEnumerable(
                ServiceDescriptor.Transient<IActionInvokerProvider, ControllerActionInvokerProvider>());

            // These are stateless
            services.TryAddSingleton<ControllerActionInvokerCache>();
            services.TryAddEnumerable(
                ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());

            //
            // Request body limit filters
            //
            services.TryAddTransient<RequestSizeLimitFilter>();
            services.TryAddTransient<DisableRequestSizeLimitFilter>();
            services.TryAddTransient<RequestFormLimitsFilter>();

            // Error description
            services.TryAddSingleton<IErrorDescriptionFactory, DefaultErrorDescriptorFactory>();

            //
            // ModelBinding, Validation
            //
            // The DefaultModelMetadataProvider does significant caching and should be a singleton.
            services.TryAddSingleton<IModelMetadataProvider, DefaultModelMetadataProvider>();
            services.TryAdd(ServiceDescriptor.Transient<ICompositeMetadataDetailsProvider>(s =>
            {
                var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
                return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders);
            }));
            services.TryAddSingleton<IModelBinderFactory, ModelBinderFactory>();
            services.TryAddSingleton<IObjectModelValidator>(s =>
            {
                var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
                var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
                return new DefaultObjectValidator(metadataProvider, options.ModelValidatorProviders);
            });
            services.TryAddSingleton<ClientValidatorCache>();
            services.TryAddSingleton<ParameterBinder>(s =>
            {
                var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
                var loggerFactory = s.GetRequiredService<ILoggerFactory>();
                var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
                var modelBinderFactory = s.GetRequiredService<IModelBinderFactory>();
                var modelValidatorProvider = new CompositeModelValidatorProvider(options.ModelValidatorProviders);
                return new ParameterBinder(metadataProvider, modelBinderFactory, modelValidatorProvider, loggerFactory);
            });

            //
            // Random Infrastructure
            //
            services.TryAddSingleton<MvcMarkerService, MvcMarkerService>();
            services.TryAddSingleton<ITypeActivatorCache, TypeActivatorCache>();
            services.TryAddSingleton<IUrlHelperFactory, UrlHelperFactory>();
            services.TryAddSingleton<IHttpRequestStreamReaderFactory, MemoryPoolHttpRequestStreamReaderFactory>();
            services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
            services.TryAddSingleton(ArrayPool<byte>.Shared);
            services.TryAddSingleton(ArrayPool<char>.Shared);
            services.TryAddSingleton<OutputFormatterSelector, DefaultOutputFormatterSelector>();
            services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<FileContentResult>, FileContentResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<RedirectResult>, RedirectResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<LocalRedirectResult>, LocalRedirectResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<RedirectToActionResult>, RedirectToActionResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
            services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();

            //
            // Route Handlers
            //
            services.TryAddSingleton<MvcRouteHandler>(); // Only one per app
            services.TryAddTransient<MvcAttributeRouteHandler>(); // Many per app

            //
            // Middleware pipeline filter related
            //
            services.TryAddSingleton<MiddlewareFilterConfigurationProvider>();
            // This maintains a cache of middleware pipelines, so it needs to be a singleton
            services.TryAddSingleton<MiddlewareFilterBuilder>();

 

代码很长,看不懂也没关系,反正你知道它添加这么一堆核心功能。

我们再来看看 AddMvc 方法。

            var builder = services.AddMvcCore();

            builder.AddApiExplorer();
            builder.AddAuthorization();

            AddDefaultFrameworkParts(builder.PartManager);

            // Order added affects options setup order

            // Default framework order
            builder.AddFormatterMappings();
            builder.AddViews();
            builder.AddRazorViewEngine();
            builder.AddRazorPages();
            builder.AddCacheTagHelper();

            // +1 order
            builder.AddDataAnnotations(); // +1 order

            // +10 order
            builder.AddJsonFormatters();

            builder.AddCors();

 

注意这句:

   var builder = services.AddMvcCore();

这说明,运行时是先调用 AddMvcCore 方法添加核心的功能后,再添加 MVC 所必备的其他功能。尤其是下面这几行,很重要。

            builder.AddViews();
            builder.AddRazorViewEngine();
            builder.AddRazorPages();

现在你明白为什么调用 AddMvcCore 方法后会找不到视图的原因了吧。

 

扯远了,咱们还是回到 Startup 类来,弄完 ConfigureServices 方法后,还要在 Configure 方法中 use 一下。

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }

别以为在 services 上面 add 完后就能用,那是两回事,services 集合仅仅说明添加功能,并不代表启用功能,UseMvc 是告诉应用程序在接收到 HTTP 请求后用 MVC 方式进行处理,些时相关的功能才会以中间件的形式插入到 HTTP 处理管道中。

你可以把 HTTP 处理管道看作一个生产线,而 services 集合中添加的内容相当于采购,我生产过程用到锄头,你帮我买,我用到馒头,你帮我买,我用到铁钳,你帮我买。至于说你买来后怎么用,用多少,那是生产线上的事情了。

你可以把 ConfigureServices 方法看作是买菜,把 Configure 方法看作是下厨。

 

这里顺便废话一下,Startup 类你是可以改为其他名字的,比如叫 MyStart,然后在 Main 入口处改一下 UseStartup 就行了。

            WebHost.CreateDefaultBuilder(args)
                .UseStartup<MyStart>()
                .Build();

运行的时候,程序会优先查找 Startup 这个名字,如果找不到再找其他的,所以,这个类名没必要改,这样还能减少程序查找的成本,反正你改了名字也没什么实际意义的,还是按照约定来吧。ConfigureServices 方法和 Configure 方法你是不能改的,因为程序会通过反射来找这两个方法。

 

说了那么多,下面进入咱们主题,我们知道,默认的约会是把视图页面放到 /Views 目录下的,并且按照 Controller 的名字建立子目录,以 Action 的名字来命名页面文件。

比如,有个 Controller 叫 Home ,里面有个 Action 叫 Test,那么默认的视图应该是这样的。

  /Views
     |--- /Home
         |--- /Test.cshtml

注意文件与目录名是严格区分大小写的,如果 Controller 是 Demo,你的目录是 demo ,是找不到视图,尤其是在 Linux 等系统上运行时,更加要严格遵守大小写的规则。

有时候,老周会觉得这样的路径不爽,目录层次套得多,老周喜欢对页面文件这样命名:Controller-Action.cshtml。例如,Controller 叫 Home,其中一个 Action 叫 Index ,那么视图页的名字就是 Home-Index.cshtml。

那么,我们该怎么修改默认的视图查找位置呢。不急,先来看看人家默认的视图查找位置。在 Configure 方法中加入以下代码。

            //app.UseMvc();
            app.Run(async context =>
            {
                // 取出选项实例
                IOptions<RazorViewEngineOptions> razoropt = app.ApplicationServices.GetService<IOptions<RazorViewEngineOptions>>();
                var locations = razoropt.Value.ViewLocationFormats;
                StringBuilder strbd = new StringBuilder();
                foreach (var item in locations)
                {
                    strbd.AppendLine(item);
                }
                // 这一行不要少,少了会乱码
                context.Response.ContentType = "text/plain;charset=utf-8";
                await context.Response.WriteAsync($"视图的默认查找位置:\n{strbd}");
            });

这里要注意一个代码约定,services 集合添加功能时,经常会附带各种选项类,而为了便于识别,选项类通常是以 Options 结尾,比如,上面代码中的 RazorViewEngineOptions。

还记得上面老周贴的源代码吗,在 AddMvc 方法中有这一句:

builder.AddRazorViewEngine();

这会使得 RazorViewEngineOptions 类的实例被加入到依赖注入的列表中,而 services 集合所添加的各种东东会合并到 app.ApplicationServices 属性上,所以,我们通过这个属性可以取出 RazorViewEngineOptions 实例,但是,你要记得:凡是选项配置类都是用 IOptions<TOptions> 泛型对象来包装,虽然它是个接口,其实现类型也许在这里。

 

依赖注入类型在注册时往往是以接口类型为 key ,这样一来我们无需考虑它有哪些实现类型,只要统一用 IOptions 接口就能获取对应的选项类实例。

所以你要记住这个约定,选项类都用 IOptiions<TOptions> 类型来包装,并且其 Value 属性中获取选项类的实例,这种约定也是为了区分类型的用途,因为所有类型都可以加入依赖注入列表中的,只有带 IOption 包装的才是选项类。

要自定义视图的查找方法,你不必要实现 IViewLocationExpander 接口,你只需要修改 RazorViewEngineOptions 类的以下三个属性即可:

1、PageViewLocationFormats:专用于 Web Pages 模型,定义查找 Razor 页面的查找位置。

2、AreaViewLocationFormats:定义带 area 的 MVC 模型的 View 页面位置。这个也许你有些陌生,一般 MVC 应用我们少加 area,它的作用可以将 MVC 模型进行分组,比如 admin 组中有 MVC,users 组中也有 MVC,只是前者不能随便访问。

3、ViewLocationFormats:这是咱们今天的重点,也是最常用的。用于定义视图的查找位置。

 

这些属性都是字符列表,可以动态增减。现在我们运行应用,看看上面的代码所输出的内容。

我们看到,默认主要查找两个目录,Views 和它的子目录 Shared。

这时候,你注意到,路径中有参数,{1} 表示 Controller 名称,{0} 表示 Action 名称。如果 Controller = Home, Action = Index,那么,查找的视图页就是 /Views/Home/Index.cshtml。

可能你又要问了,为什么参数 0 是 Action名,参数 1 是 Controller名呢,这顺序怎么是反过来的?对的,如果有 Area 的话,路径就可以变成 /{2}/Views/{1}/{0}.cshtml。

因为 Action 名是必须的,Controller 次之,Area 许多时候可以忽略,所以,Action 名字的参数位置是 0。有的视图页是不需要限定 Controller 名称的,比如以下这几个特殊页面:_Layout.cshtml、_ViewStart.cshtml、_ViewImports.cshtml。在查找这几个视图时,Action 名称直接就叫 ”_Layout“、”_ViewStart“、”_ViewImports“,不需要指定 Controller 的名字。

 

好了,知道上面这些原理,相信你也懂得怎么动手了,接下来,老周就以改为 /视图/Controller-Action.cshtml 为例。

在项目中新建两个目录,咱们来个中文名,就叫”控制器“和”视图“。

其实,Controller 类放哪儿都行,因为它们是代码,最终会参与编译的,我们要处理的主要是View。

我们写一个 DemoController 控制器,按照约定,就叫 DemoController,其实类名叫 Demo 也行的。

然后里面写一个简单的 Test 方法,作为 Action,直接返回与该 Action 关联的视图页。

    public class DemoController : Controller
    {
        public IActionResult Test()
        {
            return View();
        }
    }

 

接着,在 视图 目录下,加一个叫 Demo-Test.cshtml 的文件。注意大小写。

 

最后,很重要一步,就是在 Startup.ConfigureServices 方法中加入自定义的视图搜索路径。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddRazorOptions(opt =>
            {
                opt.ViewLocationFormats.Clear();// 清空默认的列表
                opt.ViewLocationFormats.Add("/视图/{0}" + RazorViewEngine.ViewExtension);
                opt.ViewLocationFormats.Add("/视图/{1}-{0}" + RazorViewEngine.ViewExtension);
            });
        }

这里为什么要加一条 /视图/{0}.cshtml 呢,前面说过了,有的特殊页面是只有 Action 的,如 _Layout.cshtml。RazorViewEngine.ViewExtension 是个静态字段,表示视图页的扩展名,其实就是 .cshtml,所以这里你完全可以直接写.cshtml。

 

这时候,运行程序,从 http://<your host>:<your port>/Demo/Test 访问,就能找到视图 Demo-Test.cshtml 了。输入 URL 时是不分大小写的,但是,在代码中查找视图时是区分大小写的。

但为了方便测试,我们在 UseMvc 时加个带默认值的路由。

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(r => {
                r.MapRoute("hehe", "{controller=Demo}/{action=Test}");
            });

        }

路由规则需要一个名字,这个名字有啥用,以后再告诉你。

 

此时,运行应用就很方便了,直接根 URL 上去就能看到视图了。

 

 

再补充一下问题,在 Program.cs 文件中,如果你调用的是默认的 CreateDefaultBuilder 方法是很好办的,因为它会为我们配置好一切。

        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();

 

但是,如果你自己改写了代码,比如这样。

        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                       .UseKestrel()
                       .UseStartup<Startup>()
                       .UseUrls("http://localhost:9999")
                       .Build();
            host.Run();
        }

ASP.NET Core 应用可以独立运行,Kestrel 是传说中的神兽,有了这只神兽,你可以跨平台独立运行。如果你只在 Windows 上独立,除了神兽外,你还可以用 HttpSys。这里我顺便指定了 URL ,端口是 9999。

运行后,把这个 URL 复制到浏览器可以进行访问。

 

但,你再也找不到视图了。

 

 为什么呢?因为少了一句代码。你到 \bin 目录下看看,编译只生成了.dll,并没有复制页面和其他资源,而上面的代码执行后,默认是在这个 bin 下面找资源的,所以找不到了。

解决方法是加上这一句代码。

            var host = new WebHostBuilder()
                       .UseKestrel()
                       .UseContentRoot(Directory.GetCurrentDirectory())
                       .UseStartup<Startup>()
                       .UseUrls("http://localhost:9999")
                       .Build();

 

加上这一句后,应用会自动处理当前目录的路径,调试阶段,它查找的是 VS 项目所在的目录,所以能找到视图。而在网站发布后,当前目录会自动变为 .dll 所在的目录,发布时会自动复制项目的资源。

好了,本文说到这里了,88。

 

posted @ 2018-02-04 13:12  东邪独孤  阅读(4321)  评论(1编辑  收藏  举报