Fork me on GitHub
POCO Controller

---恢复内容开始---

POCO Controller 你这么厉害,ASP.NET vNext 知道吗?

 

写在前面

阅读目录:

  POCO Controller 你这么厉害,ASP.NET vNext 知道吗?是的,ASP.NET vNext 晓得,因为这就是它的特性之一,嘿嘿。

  在上一篇《分享我对 ASP.NET vNext 的一些感受,也许多年回过头看 So Easy!》博文中,简单介绍了 ASP.NET vNext 的一些特性,文中的最后也做了一个简单的示例,在示例中 HelloWorldController 控制器像以往我们创建 ASP.NET MVC 控制器一样,需要继承 Controller 基类,就像人群中某一种特定的人(公务员),Controller 用来标示这一种人是公务员一样。后来 dudu 在评论中指出:

  ASP.NET vNext 具有 POCO Controllers 特性,控制器并不需要继承 Controller,当时试了下确实可以,后来梁兄又指出:

  本人回复:“还没接触过 POCO Controller,不做过多评论,后面深入了解下,不过多谢兄台的指教。:)”,确实对 POCO Controller 并不是很了解,但是让我对 POCO Controller 充满了好奇,后来看了下相关知识,发现确实像梁兄所说,简单的输出是可以,如果输出 View,需要自行实现一个 ViewDataDictionary 对象,如果真要做到 POCO 形式结合 View 的话,可能付出的努力要远大于使用现成的 Controller 继承?关于这一点,是有些疑问,但我觉得凡事必有存在的价值,只是了解多少而已,POCO Controller 也确实在探索性质阶段,关于 POCO Controller 以及相关知识,本篇内容希望起到抛砖引玉的作用。

  以下内容,只是一些个人看法,仅供参考学习,也欢迎讨论指教。

POCO 是什么?

POCO(Plain Old C#/CLR Object),意为:纯老式的 C#/CLR 对象,也可以称为简单的 C#/CLR 对象,POCO 的概念来自于 POJO(Plain Old Java Object),POJO 的内在含义是指那些没有从任何类继承、也没有实现任何接口,更没有被其它框架侵入的 C# 对象。

1、为什么会有 POJO?   

  主要是 Java 的开发者被 EJB 的繁杂搞怕了,大家经过反思,又回归“纯洁老式”的 JavaBean,即有无参构造函数,每个字段都有 getter 和 setter 的 java 类。

2、POJO 的意义

  POJO 让开发者可专注于业务逻辑和脱离框架的单元测试。除此之外, 由于 POJO 并不须要继承框架的类或实现其接口,开发者能够极其灵活地搭建继承结构和建造应用。

  POJO 的意义就在于它的简单而灵活性,因为它的简单和灵活,使得 POJO 能够任意扩展,从而胜任多个场合,也就让一个模型贯穿多个层成为现实。先写一个核心 POJO,然后实现业务逻辑接口和持久化接口,就成了 Domain Model; UI 需要使用时,就实现数据绑定接口,变成 VO(View Object)。

3、POJO 与 PO、VO 的区别

  • POJO 是指简单 java 对象(Plain Old Java Objects、pure old java object 或者 plain ordinary java object)。
  • PO 是指持久对象(persistant object持久对象)。
  • VO 是指值对象或者 View 对象(Value Object、View Object)。注意,本文的 VO 特指 View Object。

  持久对象实际上必须对应数据库中的 entity,所以和 POJO 有所区别。比如说 POJO 是由 new 创建,由 GC 回收。但是持久对象是 insert 数据库创建,由数据库 delete 删除的。基本上持久对象生命周期和数据库密切相关。另外持久对象往往只能存在一个数据库 Connection 之中,Connnection 关闭以后,持久对象就不存在了,而 POJO 只要不被 GC 回收,总是存在的。

  由于存在诸多差别,因此持久对象 PO(Persistent Object)在代码上肯定和 POJO 不同,起码 PO 相对于 POJO 会增加一些用来管理数据库 entity 状态的属性和方法。而 ORM 追求的目标就是要 PO 在使用上尽量和 POJO 一致,对于程序员来说,他们可以把 PO 当做 POJO 来用,而感觉不到 PO 的存在。

4、POJO 的扩展

  POJO 仅包含最简单的字段属性,没有多余的东西,它本质上就是一个普通的 JavaBean。但是在 POJO 的基础上,能够扩展出不同的对象。

  • 为 POJO 增加了持久化的方法(Insert、Update、Delete……)之后,POJO 就变成了 PO。
  • 为 POJO 增加了数据绑定功能之后,POJO 就变成了 View Object,即 UI Model。
  • 为 POJO 增加业务逻辑的方法(比如单据审核、转帐……)之后,POJO 就变成了 Domain Model。
  • POJO 还可以当作 DTO 使用。

注:以上内容来自(http://kb.cnblogs.com/page/89750/

5、POCO VS DTO

  理解了上面关于 POCO 的概念理解,如果对 DDD(领域驱动设计)有所了解,就会发现 POCO 和 DTO 某种意义上很相似,上面关于 POCO 的扩展也有指出:POCO 可以当做 DTO 使用,但也只是某种意义上的,其实有很大的区别,可以参考:http://stackoverflow.com/questions/725348/poco-vs-dto,我理解的 POCO,就像原始人类一样,它可以演化成现在的各个人种,比如白种人、黑种人和黄种人(PO、UI Model、Domain Model等等)。

  虽然 POCO 为原始人类,但是也包含(不必须)人类的状态或行为,只是这种状态或行为是最原始的,比如狩猎为生、吃生食物等等,DTO(Data Transfer Objec)是什么?关于 DTO 的概念可以参考:http://www.cnblogs.com/xishuai/p/3691787.html,DTO 是领域驱动设计中的概念,只是数据传输对象,不包含任何的行为和状态,我个人觉得和 POCO 不是一个概念里面的东西,无法进行比较。

  如果非要对 DTO 和 POCO 进行区别的话,上面把 POCO 看做为原始人类,那 DTO 可以看做是原始人类的标本,他们俩只是长得比较像,仅此而已。

Controller 是什么?

  在 ASP.NET MVC 中 Controller(控制器)的职责是:获取 Model 数据并将 Model 传递给 View 对象,通知 View 对象显示,关于 Controller 概念的官方说明:

Controllers:Controllers are the components that handle user interaction, work with the model, and ultimately select a view to render that displays UI. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. For example, the controller handles query-string values, and passes these values to the model, which in turn queries the database by using the values.

  可以把 Controller 看做是一个产品加工厂,Model 为原材料,View 为销售平台,原材料在产品加工厂经过一定的加工处理后,得到成型的产品,然后放在销售平台上进行展示销售,从这个比喻中可以看出 Controller 的作用,就是协调 Model 和 View,那 Routing(路由)是什么?可以看做是政府的宏观调控,用来决定加工生产什么产品?由哪家加工厂生产?这些工作都是 Routing 进行宏观调控的。

  为了进一步方便理解 Controller 在 ASP.NET MVC 所起到的作用,我们分析下 ASP.NET MVC 整个的处理流程,首先,用户通过 Web 浏览器向服务器发送一条 Url 请求,这个请求被 ASP.NET MVC 的路由映射系统截获。路由映射系统按照映射规则,解析出控制器名 ControllerName、ActionName和各个参数 Parameters,然后,找寻 Controllers 目录下的 ControllerNameController.cs 这个控制器类,默认情况下,系统总是找寻 Controllers 目录下的“控制器名+ Controller ”这么一个类,然后,找寻这个类下与 ActionName 同名的方法,找到后,将 Parameters 作为参数传给这个方法,而后 Action 方法开始执行,完成后返回相应视图,默认情况下,会返回 Views 目录下与 ControllerName 同名的目录下的与 ActionName 同名的 View 文件,并且将 ViewData 传递到视图。

  大致画了下 ASP.NET MVC 的处理流程:

  上图只是大致演示了下 ASP.NET MVC 的处理流程,更专业、详细请参照《Pro ASP.NET MVC Framework》作者 Steven Sanderson 的一张完整请求处理流程图(Request-Handling Pipeline Poster):

  当我们使用 VS2012 创建 MVC3/MVC4 的时候,如果把控制器中继承的 Controller 基类去掉,结果如下:

复制代码
 1 namespace MvcApplication1.Controllers
 2 {
 3     public class HomeController
 4     {
 5         //
 6         // GET: /Home/
 7         public string Index()
 8         {
 9             return "Hello World";
10         }
11     }
12 }
复制代码

  HomeController 的 IndexAction 方法中返回一段字符串,路由配置是完全正确的,运行结果会是怎样?你可能会猜到了,对,就是这样:

  结合上面关于 ASP.NET MVC 的处理流程,你会明白为什么会报“无法找到资源”的异常?而这些同样的代码或配置,在 ASP.NET vNext 中却可以正常使用,这也就是 POCO Controllers 特性的美妙之处。

  请接着往下看。

关于 POCO Controller

  理解了 POCO 和 Controller 概念,理解 POCO Controller 就不会那么困难了,网上关于 POCO Controller 特性的资料实在是少的可怜,MSDN 暂时未找到详细的说明,如果想要深入的研究,看来只有过段时间把 ASP.NET vNext 搞熟之后了,因为 ASP.NET vNext 支持 POCO Controller 特性,这样使得代码更加简洁,难道好处只是代码简洁吗?其实不然,因为 ASP.NET vNext 开源和支持内置依赖注入(DI),所以你可以研究它的源代码,在 Controller 中编写适合自己的控制器,这样使得你的 vNext 应用程序可扩展性或性能得到进一步的提升,如果可以的话,你甚至编写属于自己的一套 Controller,这一切都是从 POCO Controller 演化而来。

  其实如果往长远一点来想,我个人觉得 POCO Controller 并不简简单单是 ASP.NET vNext 的一种特性,它也代表着 ASP.NET vNext 的一种方向,因为跨平台、开源和依赖注入,使得一切都有可能,虽然 ASP.NET vNext 现在还未正式发布,POCO Controller 也仅仅是一个探路者,但是我觉得这个信号很重要,很重要,也希望大家可以接收得到。

POCO Controller 应用

1,识别 POCO Controller

  言归正题,因为 POCO Controllers 不从 Microsoft.AspNet.Mvc.Controller 基类派生,ASP.NET vNext 如何识别 POCO Controller?关于这个问题在 stackoverflow 中提问:“How Are POCO Controllers Discovered As Controllers?”,回答如下:

There are some conventions that we use to identify a POCO controller:

  • The assembly must reference MVC(必须引用 MVC)
  • The POCO controller class must have the suffix Controller(POCO controller 必须以“Controller”后缀结尾)

  必须引用 MVC 就是在 project.json 中添加如下如下引用:

2,简单 POCO Controller

  最简单的 POCO Controller:

复制代码
 1 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 2 
 3 namespace PocoControllerDemo.Controllers
 4 {
 5     public class HelloWorldController
 6     {
 7         // GET: /<controller>/
 8         public string Index()
 9         {
10             return "Hello World";
11         }
12     }
13 }
复制代码

  注意 HelloWorldController 并没有继承 Controller,这种写法在 ASP.NET vNext 是可以的,因为未涉及到 Controller 的核心操作如(View、Url、Action等),只是返回一个字符串(内置依赖注入映射默认配置),首先我们看下 DefaultControllerFactory 中的源码:

复制代码
 1 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
 2 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 
 4 using System;
 5 using Microsoft.AspNet.Mvc.Core;
 6 using Microsoft.AspNet.Mvc.ModelBinding;
 7 using Microsoft.Framework.DependencyInjection;
 8 
 9 namespace Microsoft.AspNet.Mvc
10 {
11     public class DefaultControllerFactory : IControllerFactory
12     {
13         private readonly ITypeActivator _activator;
14         private readonly IServiceProvider _serviceProvider;
15 
16         public DefaultControllerFactory(IServiceProvider serviceProvider, ITypeActivator activator)
17         {
18             _serviceProvider = serviceProvider;
19             _activator = activator;
20         }
21 
22         public object CreateController(ActionContext actionContext)
23         {
24             var actionDescriptor = actionContext.ActionDescriptor as ReflectedActionDescriptor;
25             if (actionDescriptor == null)
26             {
27                 throw new ArgumentException(
28                     Resources.FormatDefaultControllerFactory_ActionDescriptorMustBeReflected(
29                         typeof(ReflectedActionDescriptor)),
30                     "actionContext");
31             }
32 
33             var controller = _activator.CreateInstance(
34                 _serviceProvider,
35                 actionDescriptor.ControllerDescriptor.ControllerTypeInfo.AsType());
36 
37             InitializeController(controller, actionContext);
38 
39             return controller;
40         }
41 
42         public void ReleaseController(object controller)
43         {
44             var disposableController = controller as IDisposable;
45 
46             if (disposableController != null)
47             {
48                 disposableController.Dispose();
49             }
50         }
51 
52         private void InitializeController(object controller, ActionContext actionContext)
53         {
54             Injector.InjectProperty(controller, "ActionContext", actionContext);
55 
56             var viewData = new ViewDataDictionary(
57                 _serviceProvider.GetService<IModelMetadataProvider>(),
58                 actionContext.ModelState);
59             Injector.InjectProperty(controller, "ViewData", viewData);
60 
61             var urlHelper = _serviceProvider.GetService<IUrlHelper>();
62             Injector.InjectProperty(controller, "Url", urlHelper);
63 
64             Injector.CallInitializer(controller, _serviceProvider);
65         }
66     }
67 }
复制代码

3,IActionResultHelper

  InitializeController 方法,用来初始化控制器,可以看出,如果我们没有添加任何的配置操作,ASP.NET vNext 会通过内置的 IoC 容器注入默认的类型映射,比如上面的 HelloWorldController。主要包含三个类型:ActionContext、ViewDataDictionary 和 IUrlHelper,从单词上可以简单看出这三种类型所包含的意义,ViewDataDictionary 视图操作的数据字典,这个在控制器返回 View 的时候会用到,这个后面有说明,我们看下在 MSDN 中关于 POCO Controller 的简单示例,项目地址:http://www.asp.net/vnext/overview/aspnet-vnext/overview,示例代码:

复制代码
 1 public class HomeController
 2 {
 3     // Helpers can be dependency injected into the controller
 4     public HomeController(IActionResultHelper resultHelper)
 5     {
 6         Result = resultHelper;
 7     }
 8 
 9     private IActionResultHelper Result { get; set; }
10 
11     public ActionResult Index()
12     {
13         return Result.Json(new { message = "Poco controllers!" });
14     }
15 }
复制代码

  执行结果:

  代码中主要对 IActionResultHelper 进行构造函数注入,你可以实现属于自己的 IActionResultHelper 实例,使用 ASP.NET vNext 内置IoC容器进行依赖注入,IActionResultHelper 是什么?IActionResultHelper is a helper for creating action results。就是说 IActionResultHelper 用来创建 Action 结果的契约,Index 方法中返回一个 Json 格式数据,返回类型为ActionResult,Result.Json 也就是 IActionResultHelper.Json,也就是说所有的一切都表明,你可以创建属于自己的 IActionResultHelper,包含自定义的 IActionResultHelper.Json 类型格式。

  以下是在 MSDN 中找到仅存的关于 POCO Controller 的说明:

The Controller class also gives you convenient access to the HTTP request context, the current principal (IPrincipal), a view bag, and other useful properties. So the Controller class is certainly useful, but it’s not required. For a light-weight controller, you might prefer a POCO controller.

4,IModelMetadataProvider、ViewDataDictionary

  以上是有关 Action 的操作,如果涉及到 View 操作,又是怎么实现?MSDN 太坑爹了,都没有详细说明,有位国外哥们和我遇到一样的问题了,详细地址:http://geekswithblogs.net/stun/archive/2014/06/04/asp.net-vnext-blog-post-series.aspx,以下来自这位国外哥们的描述:

Today, I want to start this blog post series with a teaser code snippet for those developers familiar with the ASP.NET MVC.
Getting Started with ASP.NET MVC 6 article from ASP.NET website shows how to write a lightweight POCO (plain-old CLR object) MVC Controller class in the upcoming ASP.NET MVC 6.
However, it doesn't show us how to use the IActionResultHelper interface to render a View.
This is how I wrote my POCO MVC Controller based on the https://github.com/aspnet/Home/blob/master/samples/HelloMvc/Controllers/HomeController.cs sample from Github.

  虽然在 MSDN 中关于 View 操作没有相关说明,但是这位国外哥们搞出来了,不服不行啊,所谓前人种树,后人乘凉,来看一下这位哥们的解决方案:

复制代码
 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 using MvcSample.Web.Models;
 4 
 5 namespace MvcSample.Web
 6 {
 7     public class HomeController
 8     {
 9         IActionResultHelper html;
10         IModelMetadataProvider mmp;
11 
12         public HomeController(IActionResultHelper h, IModelMetadataProvider mmp)
13         {
14             this.html = h;
15             this.mmp = mmp;
16         }
17 
18         public IActionResult Index()
19         {
20             var viewData = new ViewDataDictionary<User>(mmp) { Model = User() };
21             return html.View("Index", viewData);
22         }
23 
24         public User User()
25         {
26             return new User { Name = "My name", Address = "My address" };
27         }
28     }
29 }
复制代码

  可以看出除了 IActionResultHelper 类型映射,还包含 IModelMetadataProvider 类型,关于 IModelMetadataProvider 可以从 DefaultControllerFactory 源码中得到一些信息,在创建 ViewDataDictionary 实例的时候,需要传入 IModelMetadataProvider 类型的参数,也就是代码中的 new ViewDataDictionary<User>(mmp) { Model = User() }; IModelMetadataProvider 需要引入 using Microsoft.AspNet.Mvc.ModelBinding,关于 IModelMetadataProvider 定义,请参照:

复制代码
 1 using System;
 2 using System.Collections.Generic;
 3 
 4 namespace Microsoft.AspNet.Mvc.ModelBinding
 5 {
 6     public interface IModelMetadataProvider
 7     {
 8         IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
 9         ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
10         ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
11     }
12 }
复制代码

  html.View("Index", viewData); 使用方式就像我们平常向 View 传入数据一样,如果不传入数据,但是也要传入 ViewDataDictionary 类型参数,修改 HelloWorldController 控制器代码:

复制代码
 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 
 4 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 5 
 6 namespace PocoControllerDemo.Controllers
 7 {
 8     public class HelloWorldController
 9     {
10         private IActionResultHelper actionResultHelper { get; set; }
11         private IModelMetadataProvider modelMetadataProvider { get; set; }
12         // Helpers can be dependency injected into the controller
13         public HelloWorldController(IActionResultHelper resultHelper, IModelMetadataProvider metadataProvider)
14         {
15             actionResultHelper = resultHelper;
16             modelMetadataProvider = metadataProvider;
17         }
18         // GET: /<controller>/
19         public ActionResult Index()
20         {
21             //return Result.Json(new { message = "Poco controllers!" });
22             var viewData = new ViewDataDictionary(modelMetadataProvider);
23             return actionResultHelper.View("Index", viewData);
24         }
25     }
26 }
复制代码

  Index.cshtml 视图中只有 html 代码:“<h1>Hello World</h1>”,运行结果:

5,IActionDiscoveryConventions、IControllerDescriptorFactory

  既然 POCO Controller 都这么厉害了(以上内容),还能不能来点更厉害的呢?当然会有,比如扩展 EndpointActionDiscoveryConventions 和IControllerDescriptorFactory 使得控制器并不需要以“Controller”名称结尾,可以按照自己的喜好创建配置结束符,虽然功能作用不是很大,但是这些操作在 MVC3/MVC4 中连想都不敢想,也从另一方便看出 POCO Controller 可扩展型是多么的强大,怎么实现?

  首先创建 EndpointActionDiscoveryConventions 类,继承 DefaultActionDiscoveryConventions(IActionDiscoveryConventions 的默认派生类),在 IActionDiscoveryConventions 接口定义中有个重要的虚方法:IsController,参数类型为:TypeInfo,用来判断当前 typeInfo 是否为 Controller?示例代码: 

复制代码
 1 using System;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Mvc;
 4 
 5 namespace PocoControllerDemo
 6 {
 7     public class EndpointActionDiscoveryConventions : DefaultActionDiscoveryConventions
 8     {
 9         public override bool IsController(TypeInfo typeInfo)
10         {
11             var isController = base.IsController(typeInfo);
12             return isController || typeInfo.Name.EndsWith("Endpoint", StringComparison.OrdinalIgnoreCase);
13         }
14     }
15 }
复制代码

  可以看出 IsController 控制器判断,以“Endpoint”为结束符的 TypeInfo 返回 True,表示当前 TypeInfo 为 Controller。仅仅实现 Controller 判断还是不够的,还要对 IControllerDescriptorFactory 进行配置,示例代码:

复制代码
 1 using System;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Mvc;
 4 
 5 namespace PocoControllerDemo
 6 {
 7     public class EndpointControllerDescriptorFactory : IControllerDescriptorFactory
 8     {
 9         public ControllerDescriptor CreateControllerDescriptor(TypeInfo type)
10         {
11             var descriptor = new ControllerDescriptor(type);
12 
13             if (descriptor.Name.EndsWith("Endpoint", StringComparison.Ordinal))
14             {
15                 var nameProp = descriptor.GetType().GetProperty("Name");
16                 var oldName = nameProp.GetValue(descriptor, null) as string;
17                 nameProp.SetValue(descriptor, oldName.ToLowerInvariant().Replace("endpoint", string.Empty));
18             }
19             return descriptor;
20         }
21     }
22 }
复制代码

  HelloWorldEndpoint 控制器代码:

复制代码
 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 
 4 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 5 
 6 namespace PocoControllerDemo.Controllers
 7 {
 8     public class HelloWorldEndpoint
 9     {
10         private IActionResultHelper actionResultHelper { get; set; }
11         private IModelMetadataProvider modelMetadataProvider { get; set; }
12         // Helpers can be dependency injected into the controller
13         public HelloWorldEndpoint(IActionResultHelper resultHelper, IModelMetadataProvider metadataProvider)
14         {
15             actionResultHelper = resultHelper;
16             modelMetadataProvider = metadataProvider;
17         }
18         // GET: /<controller>/
19         public ActionResult Index()
20         {
21             //return Result.Json(new { message = "Poco controllers!" });
22             var viewData = new ViewDataDictionary(modelMetadataProvider);
23             return actionResultHelper.View("Index", viewData);
24         }
25     }
26 }
复制代码

  注意 HelloWorldEndpoint 并不是 HelloWorldController,Startup 添加 IActionDiscoveryConventions 和 IControllerDescriptorFactory 类型配置:

1             app.UseServices(services =>
2             {
3                 services.AddMvc();
4                 services.AddTransient<IActionDiscoveryConventions, EndpointActionDiscoveryConventions>();
5                 services.AddTransient<IControllerDescriptorFactory, EndpointControllerDescriptorFactory>();
6             });

  运行截图:

后记

  示例代码下载:http://pan.baidu.com/s/1gd8AoxH

  ASP.NET vNext 是 .NET 未来发展的一种方向,那我觉得 POCO Controller 就是 ASP.NET vNext 未来发展的一种方向,还是那句话,因为跨平台、开源和依赖注入,使一切皆有可能,关于 ASP.NET vNext 的相关探讨,未完待续。

  再次在 MSDN 中搜“ASP.NET vNext”,虽然过去了才几天的时间,发现已经又增加了几百篇的文章,看来全球的程序员对 ASP.NET vNext 还是充满热情的,也希望博客园的园友们多多奉献,更期待 ASP.NET vNext 正式版发布早日到来。

  如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^

  参考资料:

---恢复内容结束---

POCO Controller 你这么厉害,ASP.NET vNext 知道吗?

 

写在前面

阅读目录:

  POCO Controller 你这么厉害,ASP.NET vNext 知道吗?是的,ASP.NET vNext 晓得,因为这就是它的特性之一,嘿嘿。

  在上一篇《分享我对 ASP.NET vNext 的一些感受,也许多年回过头看 So Easy!》博文中,简单介绍了 ASP.NET vNext 的一些特性,文中的最后也做了一个简单的示例,在示例中 HelloWorldController 控制器像以往我们创建 ASP.NET MVC 控制器一样,需要继承 Controller 基类,就像人群中某一种特定的人(公务员),Controller 用来标示这一种人是公务员一样。后来 dudu 在评论中指出:

  ASP.NET vNext 具有 POCO Controllers 特性,控制器并不需要继承 Controller,当时试了下确实可以,后来梁兄又指出:

  本人回复:“还没接触过 POCO Controller,不做过多评论,后面深入了解下,不过多谢兄台的指教。:)”,确实对 POCO Controller 并不是很了解,但是让我对 POCO Controller 充满了好奇,后来看了下相关知识,发现确实像梁兄所说,简单的输出是可以,如果输出 View,需要自行实现一个 ViewDataDictionary 对象,如果真要做到 POCO 形式结合 View 的话,可能付出的努力要远大于使用现成的 Controller 继承?关于这一点,是有些疑问,但我觉得凡事必有存在的价值,只是了解多少而已,POCO Controller 也确实在探索性质阶段,关于 POCO Controller 以及相关知识,本篇内容希望起到抛砖引玉的作用。

  以下内容,只是一些个人看法,仅供参考学习,也欢迎讨论指教。

POCO 是什么?

POCO(Plain Old C#/CLR Object),意为:纯老式的 C#/CLR 对象,也可以称为简单的 C#/CLR 对象,POCO 的概念来自于 POJO(Plain Old Java Object),POJO 的内在含义是指那些没有从任何类继承、也没有实现任何接口,更没有被其它框架侵入的 C# 对象。

1、为什么会有 POJO?   

  主要是 Java 的开发者被 EJB 的繁杂搞怕了,大家经过反思,又回归“纯洁老式”的 JavaBean,即有无参构造函数,每个字段都有 getter 和 setter 的 java 类。

2、POJO 的意义

  POJO 让开发者可专注于业务逻辑和脱离框架的单元测试。除此之外, 由于 POJO 并不须要继承框架的类或实现其接口,开发者能够极其灵活地搭建继承结构和建造应用。

  POJO 的意义就在于它的简单而灵活性,因为它的简单和灵活,使得 POJO 能够任意扩展,从而胜任多个场合,也就让一个模型贯穿多个层成为现实。先写一个核心 POJO,然后实现业务逻辑接口和持久化接口,就成了 Domain Model; UI 需要使用时,就实现数据绑定接口,变成 VO(View Object)。

3、POJO 与 PO、VO 的区别

  • POJO 是指简单 java 对象(Plain Old Java Objects、pure old java object 或者 plain ordinary java object)。
  • PO 是指持久对象(persistant object持久对象)。
  • VO 是指值对象或者 View 对象(Value Object、View Object)。注意,本文的 VO 特指 View Object。

  持久对象实际上必须对应数据库中的 entity,所以和 POJO 有所区别。比如说 POJO 是由 new 创建,由 GC 回收。但是持久对象是 insert 数据库创建,由数据库 delete 删除的。基本上持久对象生命周期和数据库密切相关。另外持久对象往往只能存在一个数据库 Connection 之中,Connnection 关闭以后,持久对象就不存在了,而 POJO 只要不被 GC 回收,总是存在的。

  由于存在诸多差别,因此持久对象 PO(Persistent Object)在代码上肯定和 POJO 不同,起码 PO 相对于 POJO 会增加一些用来管理数据库 entity 状态的属性和方法。而 ORM 追求的目标就是要 PO 在使用上尽量和 POJO 一致,对于程序员来说,他们可以把 PO 当做 POJO 来用,而感觉不到 PO 的存在。

4、POJO 的扩展

  POJO 仅包含最简单的字段属性,没有多余的东西,它本质上就是一个普通的 JavaBean。但是在 POJO 的基础上,能够扩展出不同的对象。

  • 为 POJO 增加了持久化的方法(Insert、Update、Delete……)之后,POJO 就变成了 PO。
  • 为 POJO 增加了数据绑定功能之后,POJO 就变成了 View Object,即 UI Model。
  • 为 POJO 增加业务逻辑的方法(比如单据审核、转帐……)之后,POJO 就变成了 Domain Model。
  • POJO 还可以当作 DTO 使用。

注:以上内容来自(http://kb.cnblogs.com/page/89750/

5、POCO VS DTO

  理解了上面关于 POCO 的概念理解,如果对 DDD(领域驱动设计)有所了解,就会发现 POCO 和 DTO 某种意义上很相似,上面关于 POCO 的扩展也有指出:POCO 可以当做 DTO 使用,但也只是某种意义上的,其实有很大的区别,可以参考:http://stackoverflow.com/questions/725348/poco-vs-dto,我理解的 POCO,就像原始人类一样,它可以演化成现在的各个人种,比如白种人、黑种人和黄种人(PO、UI Model、Domain Model等等)。

  虽然 POCO 为原始人类,但是也包含(不必须)人类的状态或行为,只是这种状态或行为是最原始的,比如狩猎为生、吃生食物等等,DTO(Data Transfer Objec)是什么?关于 DTO 的概念可以参考:http://www.cnblogs.com/xishuai/p/3691787.html,DTO 是领域驱动设计中的概念,只是数据传输对象,不包含任何的行为和状态,我个人觉得和 POCO 不是一个概念里面的东西,无法进行比较。

  如果非要对 DTO 和 POCO 进行区别的话,上面把 POCO 看做为原始人类,那 DTO 可以看做是原始人类的标本,他们俩只是长得比较像,仅此而已。

Controller 是什么?

  在 ASP.NET MVC 中 Controller(控制器)的职责是:获取 Model 数据并将 Model 传递给 View 对象,通知 View 对象显示,关于 Controller 概念的官方说明:

Controllers:Controllers are the components that handle user interaction, work with the model, and ultimately select a view to render that displays UI. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. For example, the controller handles query-string values, and passes these values to the model, which in turn queries the database by using the values.

  可以把 Controller 看做是一个产品加工厂,Model 为原材料,View 为销售平台,原材料在产品加工厂经过一定的加工处理后,得到成型的产品,然后放在销售平台上进行展示销售,从这个比喻中可以看出 Controller 的作用,就是协调 Model 和 View,那 Routing(路由)是什么?可以看做是政府的宏观调控,用来决定加工生产什么产品?由哪家加工厂生产?这些工作都是 Routing 进行宏观调控的。

  为了进一步方便理解 Controller 在 ASP.NET MVC 所起到的作用,我们分析下 ASP.NET MVC 整个的处理流程,首先,用户通过 Web 浏览器向服务器发送一条 Url 请求,这个请求被 ASP.NET MVC 的路由映射系统截获。路由映射系统按照映射规则,解析出控制器名 ControllerName、ActionName和各个参数 Parameters,然后,找寻 Controllers 目录下的 ControllerNameController.cs 这个控制器类,默认情况下,系统总是找寻 Controllers 目录下的“控制器名+ Controller ”这么一个类,然后,找寻这个类下与 ActionName 同名的方法,找到后,将 Parameters 作为参数传给这个方法,而后 Action 方法开始执行,完成后返回相应视图,默认情况下,会返回 Views 目录下与 ControllerName 同名的目录下的与 ActionName 同名的 View 文件,并且将 ViewData 传递到视图。

  大致画了下 ASP.NET MVC 的处理流程:

  上图只是大致演示了下 ASP.NET MVC 的处理流程,更专业、详细请参照《Pro ASP.NET MVC Framework》作者 Steven Sanderson 的一张完整请求处理流程图(Request-Handling Pipeline Poster):

  当我们使用 VS2012 创建 MVC3/MVC4 的时候,如果把控制器中继承的 Controller 基类去掉,结果如下:

复制代码
 1 namespace MvcApplication1.Controllers
 2 {
 3     public class HomeController
 4     {
 5         //
 6         // GET: /Home/
 7         public string Index()
 8         {
 9             return "Hello World";
10         }
11     }
12 }
复制代码

  HomeController 的 IndexAction 方法中返回一段字符串,路由配置是完全正确的,运行结果会是怎样?你可能会猜到了,对,就是这样:

  结合上面关于 ASP.NET MVC 的处理流程,你会明白为什么会报“无法找到资源”的异常?而这些同样的代码或配置,在 ASP.NET vNext 中却可以正常使用,这也就是 POCO Controllers 特性的美妙之处。

  请接着往下看。

关于 POCO Controller

  理解了 POCO 和 Controller 概念,理解 POCO Controller 就不会那么困难了,网上关于 POCO Controller 特性的资料实在是少的可怜,MSDN 暂时未找到详细的说明,如果想要深入的研究,看来只有过段时间把 ASP.NET vNext 搞熟之后了,因为 ASP.NET vNext 支持 POCO Controller 特性,这样使得代码更加简洁,难道好处只是代码简洁吗?其实不然,因为 ASP.NET vNext 开源和支持内置依赖注入(DI),所以你可以研究它的源代码,在 Controller 中编写适合自己的控制器,这样使得你的 vNext 应用程序可扩展性或性能得到进一步的提升,如果可以的话,你甚至编写属于自己的一套 Controller,这一切都是从 POCO Controller 演化而来。

  其实如果往长远一点来想,我个人觉得 POCO Controller 并不简简单单是 ASP.NET vNext 的一种特性,它也代表着 ASP.NET vNext 的一种方向,因为跨平台、开源和依赖注入,使得一切都有可能,虽然 ASP.NET vNext 现在还未正式发布,POCO Controller 也仅仅是一个探路者,但是我觉得这个信号很重要,很重要,也希望大家可以接收得到。

POCO Controller 应用

1,识别 POCO Controller

  言归正题,因为 POCO Controllers 不从 Microsoft.AspNet.Mvc.Controller 基类派生,ASP.NET vNext 如何识别 POCO Controller?关于这个问题在 stackoverflow 中提问:“How Are POCO Controllers Discovered As Controllers?”,回答如下:

There are some conventions that we use to identify a POCO controller:

  • The assembly must reference MVC(必须引用 MVC)
  • The POCO controller class must have the suffix Controller(POCO controller 必须以“Controller”后缀结尾)

  必须引用 MVC 就是在 project.json 中添加如下如下引用:

2,简单 POCO Controller

  最简单的 POCO Controller:

复制代码
 1 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 2 
 3 namespace PocoControllerDemo.Controllers
 4 {
 5     public class HelloWorldController
 6     {
 7         // GET: /<controller>/
 8         public string Index()
 9         {
10             return "Hello World";
11         }
12     }
13 }
复制代码

  注意 HelloWorldController 并没有继承 Controller,这种写法在 ASP.NET vNext 是可以的,因为未涉及到 Controller 的核心操作如(View、Url、Action等),只是返回一个字符串(内置依赖注入映射默认配置),首先我们看下 DefaultControllerFactory 中的源码:

复制代码
 1 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
 2 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 
 4 using System;
 5 using Microsoft.AspNet.Mvc.Core;
 6 using Microsoft.AspNet.Mvc.ModelBinding;
 7 using Microsoft.Framework.DependencyInjection;
 8 
 9 namespace Microsoft.AspNet.Mvc
10 {
11     public class DefaultControllerFactory : IControllerFactory
12     {
13         private readonly ITypeActivator _activator;
14         private readonly IServiceProvider _serviceProvider;
15 
16         public DefaultControllerFactory(IServiceProvider serviceProvider, ITypeActivator activator)
17         {
18             _serviceProvider = serviceProvider;
19             _activator = activator;
20         }
21 
22         public object CreateController(ActionContext actionContext)
23         {
24             var actionDescriptor = actionContext.ActionDescriptor as ReflectedActionDescriptor;
25             if (actionDescriptor == null)
26             {
27                 throw new ArgumentException(
28                     Resources.FormatDefaultControllerFactory_ActionDescriptorMustBeReflected(
29                         typeof(ReflectedActionDescriptor)),
30                     "actionContext");
31             }
32 
33             var controller = _activator.CreateInstance(
34                 _serviceProvider,
35                 actionDescriptor.ControllerDescriptor.ControllerTypeInfo.AsType());
36 
37             InitializeController(controller, actionContext);
38 
39             return controller;
40         }
41 
42         public void ReleaseController(object controller)
43         {
44             var disposableController = controller as IDisposable;
45 
46             if (disposableController != null)
47             {
48                 disposableController.Dispose();
49             }
50         }
51 
52         private void InitializeController(object controller, ActionContext actionContext)
53         {
54             Injector.InjectProperty(controller, "ActionContext", actionContext);
55 
56             var viewData = new ViewDataDictionary(
57                 _serviceProvider.GetService<IModelMetadataProvider>(),
58                 actionContext.ModelState);
59             Injector.InjectProperty(controller, "ViewData", viewData);
60 
61             var urlHelper = _serviceProvider.GetService<IUrlHelper>();
62             Injector.InjectProperty(controller, "Url", urlHelper);
63 
64             Injector.CallInitializer(controller, _serviceProvider);
65         }
66     }
67 }
复制代码

3,IActionResultHelper

  InitializeController 方法,用来初始化控制器,可以看出,如果我们没有添加任何的配置操作,ASP.NET vNext 会通过内置的 IoC 容器注入默认的类型映射,比如上面的 HelloWorldController。主要包含三个类型:ActionContext、ViewDataDictionary 和 IUrlHelper,从单词上可以简单看出这三种类型所包含的意义,ViewDataDictionary 视图操作的数据字典,这个在控制器返回 View 的时候会用到,这个后面有说明,我们看下在 MSDN 中关于 POCO Controller 的简单示例,项目地址:http://www.asp.net/vnext/overview/aspnet-vnext/overview,示例代码:

复制代码
 1 public class HomeController
 2 {
 3     // Helpers can be dependency injected into the controller
 4     public HomeController(IActionResultHelper resultHelper)
 5     {
 6         Result = resultHelper;
 7     }
 8 
 9     private IActionResultHelper Result { get; set; }
10 
11     public ActionResult Index()
12     {
13         return Result.Json(new { message = "Poco controllers!" });
14     }
15 }
复制代码

  执行结果:

  代码中主要对 IActionResultHelper 进行构造函数注入,你可以实现属于自己的 IActionResultHelper 实例,使用 ASP.NET vNext 内置IoC容器进行依赖注入,IActionResultHelper 是什么?IActionResultHelper is a helper for creating action results。就是说 IActionResultHelper 用来创建 Action 结果的契约,Index 方法中返回一个 Json 格式数据,返回类型为ActionResult,Result.Json 也就是 IActionResultHelper.Json,也就是说所有的一切都表明,你可以创建属于自己的 IActionResultHelper,包含自定义的 IActionResultHelper.Json 类型格式。

  以下是在 MSDN 中找到仅存的关于 POCO Controller 的说明:

The Controller class also gives you convenient access to the HTTP request context, the current principal (IPrincipal), a view bag, and other useful properties. So the Controller class is certainly useful, but it’s not required. For a light-weight controller, you might prefer a POCO controller.

4,IModelMetadataProvider、ViewDataDictionary

  以上是有关 Action 的操作,如果涉及到 View 操作,又是怎么实现?MSDN 太坑爹了,都没有详细说明,有位国外哥们和我遇到一样的问题了,详细地址:http://geekswithblogs.net/stun/archive/2014/06/04/asp.net-vnext-blog-post-series.aspx,以下来自这位国外哥们的描述:

Today, I want to start this blog post series with a teaser code snippet for those developers familiar with the ASP.NET MVC.
Getting Started with ASP.NET MVC 6 article from ASP.NET website shows how to write a lightweight POCO (plain-old CLR object) MVC Controller class in the upcoming ASP.NET MVC 6.
However, it doesn't show us how to use the IActionResultHelper interface to render a View.
This is how I wrote my POCO MVC Controller based on the https://github.com/aspnet/Home/blob/master/samples/HelloMvc/Controllers/HomeController.cs sample from Github.

  虽然在 MSDN 中关于 View 操作没有相关说明,但是这位国外哥们搞出来了,不服不行啊,所谓前人种树,后人乘凉,来看一下这位哥们的解决方案:

复制代码
 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 using MvcSample.Web.Models;
 4 
 5 namespace MvcSample.Web
 6 {
 7     public class HomeController
 8     {
 9         IActionResultHelper html;
10         IModelMetadataProvider mmp;
11 
12         public HomeController(IActionResultHelper h, IModelMetadataProvider mmp)
13         {
14             this.html = h;
15             this.mmp = mmp;
16         }
17 
18         public IActionResult Index()
19         {
20             var viewData = new ViewDataDictionary<User>(mmp) { Model = User() };
21             return html.View("Index", viewData);
22         }
23 
24         public User User()
25         {
26             return new User { Name = "My name", Address = "My address" };
27         }
28     }
29 }
复制代码

  可以看出除了 IActionResultHelper 类型映射,还包含 IModelMetadataProvider 类型,关于 IModelMetadataProvider 可以从 DefaultControllerFactory 源码中得到一些信息,在创建 ViewDataDictionary 实例的时候,需要传入 IModelMetadataProvider 类型的参数,也就是代码中的 new ViewDataDictionary<User>(mmp) { Model = User() }; IModelMetadataProvider 需要引入 using Microsoft.AspNet.Mvc.ModelBinding,关于 IModelMetadataProvider 定义,请参照:

复制代码
 1 using System;
 2 using System.Collections.Generic;
 3 
 4 namespace Microsoft.AspNet.Mvc.ModelBinding
 5 {
 6     public interface IModelMetadataProvider
 7     {
 8         IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
 9         ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
10         ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
11     }
12 }
复制代码

  html.View("Index", viewData); 使用方式就像我们平常向 View 传入数据一样,如果不传入数据,但是也要传入 ViewDataDictionary 类型参数,修改 HelloWorldController 控制器代码:

复制代码
 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 
 4 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 5 
 6 namespace PocoControllerDemo.Controllers
 7 {
 8     public class HelloWorldController
 9     {
10         private IActionResultHelper actionResultHelper { get; set; }
11         private IModelMetadataProvider modelMetadataProvider { get; set; }
12         // Helpers can be dependency injected into the controller
13         public HelloWorldController(IActionResultHelper resultHelper, IModelMetadataProvider metadataProvider)
14         {
15             actionResultHelper = resultHelper;
16             modelMetadataProvider = metadataProvider;
17         }
18         // GET: /<controller>/
19         public ActionResult Index()
20         {
21             //return Result.Json(new { message = "Poco controllers!" });
22             var viewData = new ViewDataDictionary(modelMetadataProvider);
23             return actionResultHelper.View("Index", viewData);
24         }
25     }
26 }
复制代码

  Index.cshtml 视图中只有 html 代码:“<h1>Hello World</h1>”,运行结果:

5,IActionDiscoveryConventions、IControllerDescriptorFactory

  既然 POCO Controller 都这么厉害了(以上内容),还能不能来点更厉害的呢?当然会有,比如扩展 EndpointActionDiscoveryConventions 和IControllerDescriptorFactory 使得控制器并不需要以“Controller”名称结尾,可以按照自己的喜好创建配置结束符,虽然功能作用不是很大,但是这些操作在 MVC3/MVC4 中连想都不敢想,也从另一方便看出 POCO Controller 可扩展型是多么的强大,怎么实现?

  首先创建 EndpointActionDiscoveryConventions 类,继承 DefaultActionDiscoveryConventions(IActionDiscoveryConventions 的默认派生类),在 IActionDiscoveryConventions 接口定义中有个重要的虚方法:IsController,参数类型为:TypeInfo,用来判断当前 typeInfo 是否为 Controller?示例代码: 

复制代码
 1 using System;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Mvc;
 4 
 5 namespace PocoControllerDemo
 6 {
 7     public class EndpointActionDiscoveryConventions : DefaultActionDiscoveryConventions
 8     {
 9         public override bool IsController(TypeInfo typeInfo)
10         {
11             var isController = base.IsController(typeInfo);
12             return isController || typeInfo.Name.EndsWith("Endpoint", StringComparison.OrdinalIgnoreCase);
13         }
14     }
15 }
复制代码

  可以看出 IsController 控制器判断,以“Endpoint”为结束符的 TypeInfo 返回 True,表示当前 TypeInfo 为 Controller。仅仅实现 Controller 判断还是不够的,还要对 IControllerDescriptorFactory 进行配置,示例代码:

复制代码
 1 using System;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Mvc;
 4 
 5 namespace PocoControllerDemo
 6 {
 7     public class EndpointControllerDescriptorFactory : IControllerDescriptorFactory
 8     {
 9         public ControllerDescriptor CreateControllerDescriptor(TypeInfo type)
10         {
11             var descriptor = new ControllerDescriptor(type);
12 
13             if (descriptor.Name.EndsWith("Endpoint", StringComparison.Ordinal))
14             {
15                 var nameProp = descriptor.GetType().GetProperty("Name");
16                 var oldName = nameProp.GetValue(descriptor, null) as string;
17                 nameProp.SetValue(descriptor, oldName.ToLowerInvariant().Replace("endpoint", string.Empty));
18             }
19             return descriptor;
20         }
21     }
22 }
复制代码

  HelloWorldEndpoint 控制器代码:

复制代码
 1 using Microsoft.AspNet.Mvc;
 2 using Microsoft.AspNet.Mvc.ModelBinding;
 3 
 4 // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
 5 
 6 namespace PocoControllerDemo.Controllers
 7 {
 8     public class HelloWorldEndpoint
 9     {
10         private IActionResultHelper actionResultHelper { get; set; }
11         private IModelMetadataProvider modelMetadataProvider { get; set; }
12         // Helpers can be dependency injected into the controller
13         public HelloWorldEndpoint(IActionResultHelper resultHelper, IModelMetadataProvider metadataProvider)
14         {
15             actionResultHelper = resultHelper;
16             modelMetadataProvider = metadataProvider;
17         }
18         // GET: /<controller>/
19         public ActionResult Index()
20         {
21             //return Result.Json(new { message = "Poco controllers!" });
22             var viewData = new ViewDataDictionary(modelMetadataProvider);
23             return actionResultHelper.View("Index", viewData);
24         }
25     }
26 }
复制代码

  注意 HelloWorldEndpoint 并不是 HelloWorldController,Startup 添加 IActionDiscoveryConventions 和 IControllerDescriptorFactory 类型配置:

1             app.UseServices(services =>
2             {
3                 services.AddMvc();
4                 services.AddTransient<IActionDiscoveryConventions, EndpointActionDiscoveryConventions>();
5                 services.AddTransient<IControllerDescriptorFactory, EndpointControllerDescriptorFactory>();
6             });

  运行截图:

后记

  示例代码下载:http://pan.baidu.com/s/1gd8AoxH

  ASP.NET vNext 是 .NET 未来发展的一种方向,那我觉得 POCO Controller 就是 ASP.NET vNext 未来发展的一种方向,还是那句话,因为跨平台、开源和依赖注入,使一切皆有可能,关于 ASP.NET vNext 的相关探讨,未完待续。

  再次在 MSDN 中搜“ASP.NET vNext”,虽然过去了才几天的时间,发现已经又增加了几百篇的文章,看来全球的程序员对 ASP.NET vNext 还是充满热情的,也希望博客园的园友们多多奉献,更期待 ASP.NET vNext 正式版发布早日到来。

  如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^

  参考资料:

posted on 2014-06-16 17:15  HackerVirus  阅读(549)  评论(0编辑  收藏  举报