zoukankan      html  css  js  c++  java
  • 【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($"视图的默认查找位置:
    {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 复制到浏览器可以进行访问。

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

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

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

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

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

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

  • 相关阅读:
    VUE-cli使用
    2017/04/09王晨分享课大纲
    CommonJS模块和ES6模块的区别
    css常见布局方式
    从输入 URL 到页面加载完成的过程中都发生了什么
    函数节流与防抖的实现
    JavaScript表单
    jQuery方法实现
    移动端去除横向滚动条
    请假时碰到法定假期,实际请假几天?
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/8412827.html
Copyright © 2011-2022 走看看