zoukankan      html  css  js  c++  java
  • Asp.Net Mvc 自定义扩展

    目录:

    1. 自定义模型IModelBinder
    2. 自定义模型验证
    3. 自定义视图引擎
    4. 自定义Html辅助方法
    5. 自定义Razor辅助方法
    6. 自定义Ajax辅助方法
    7. 自定义控制器扩展
    8. 自定义过滤器
    9. 自定义ActionResult

    自定义模型IModelBinder

    IModelBinder主要解决的问题是,将请求的数据转换成需要的数据这部分逻辑进行了封装。比如说 http://localhost:4742/Person/Person/2 请求的参数Id是2,通过参数2,获得2相关的所有数据。

    这样做的好处是:

    1. 使代码变得更加简洁
    2. 帮助我们获取HTTP请求中的数据
    3. 帮助我们完成必要的数据类型转换

    Controller部分:

    当访问http://localhost:4742/Person/Person?Name=admin&Age=12时,自动转换为setting对象

    [HttpPost]
        public ActionResult Person2([ModelBinder(typeof(SettingsBinder))]Settings settings)
        {       
            return View("Person");
        }

    IModelBinder部分

    SettingsBinder继承了IModelBinder,并实现了BindModel,通过controllerContext.HttpContext.Request获取请求,从请求中获取参数值,然后转换为对象

    public class SettingsBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var request = controllerContext.HttpContext.Request;
            //  从request中获取参数值
            var name = request["Name"];
            var age = request["Age"];
            //  然后将参数值转为对象
            var setting = new { Name = name, Age = age };
            return setting;
        }
    }

    注:当Person2([ModelBinder(typeof(SettingsBinder))]Settings settings)已存在时,不能再定义Person2方法

    自定义验证

    通过定义自定义验证特性类,实现View的模型验证,NotEqualTo即自定义验证

    public class RegisterModel
        {
            [Required]
            [StringLength(6, MinimumLength = 2)] //
            [Display(Name = "用户名")]
            public string UserName { get; set; }
    
    
            [NotEqualTo("UserName", ErrorMessage = "不能与用户名的值相同")]
            public string OtherName { get; set; }
    
            //  NotEqualTo  是自定义模型验证特性
        }

    NotEqualToAttribute继承了ValidationAttribute和IClientValidatable ,并实现了IsValid和GetClientValidationRules方法

    通过NotEqualTo构造参数的值UserName,获得该对象对应该参数值的属性值,匹配属性的值和约束的值OtherName是否相等,然后返回结果信息

    using System.ComponentModel.DataAnnotations;
    using System.Globalization;
    using System.Web.Mvc;
    
    namespace MvcValidation.Extension
    {
        //  ValidationAttribute 验证特性
        //  IClientValidatable  客户端验证接口(View视图验证)
        public class NotEqualToAttribute : ValidationAttribute, IClientValidatable
        {
            public string OtherProperty { get; set; }
    
            //  构造参数
            public NotEqualToAttribute(string otherProperty)
            {
                OtherProperty = otherProperty;
            }
    
            //  验证方法
            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                //从验证上下文中可以获取我们想要的的属性
                var property = validationContext.ObjectType.GetProperty(OtherProperty);
                if (property == null)
                {
                    return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "{0} 不存在", OtherProperty));
                }
    
                //获取属性的值
                var otherValue = property.GetValue(validationContext.ObjectInstance, null);
    
                //  判断并返回验证结果
                if (object.Equals(value, otherValue))
                {
                    return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
                }
                return null;
            }
            //  客户端验证
            public System.Collections.Generic.IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
            {
                //  设置客户端验证结果信息
                var rule = new ModelClientValidationRule
                {
                    ValidationType = "notequalto",
                    ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
                };
                rule.ValidationParameters["other"] = OtherProperty;
                yield return rule;
            }
        }
    }


    自定义视图引擎

    系统提供了视图和视图引擎,我们需要了解它之后继承并重写它的逻辑。

    /*  
         *  思路
         *  1、控制器方法返回ActionResult是一个抽象类 
         *  2、ActionResult的其中一个子类ViewResult,正是她使用IView实例最终渲染出视图 
         *  3、需要自定义IView 
         *  4、IViewEngine管理着IView,同时也需要自定义IViewEngine
         *  5、自定义IViewEngine是需要全局注册的
         */
    //namespace System.Web.Mvc
    
        //public interface IView
        //{
        //  第一个参数ViewContext包含了需要被渲染的信息,被传递到前台的强类型Model也包含在其中。 
        //  第二个参数TextWriter可以帮助我们写出想要的html格式。
        //  void Render(ViewContext viewContent, TextWriter textWriter);
        //}
    //namespace System.Web.Mvc
        //public interface IViewEngine
        //{
        //    //  FindPartialView:在当前控制器上下文ControllerContext中找到部分视图。 
        //    System.Web.Mvc.ViewEngineResult FindPartialView(System.Web.Mvc.ControllerContext controllerContext, string partialViewName, bool useCache);
        //    //  FindView:在当前控制器上下文ControllerContext中找到视图。 
        //    System.Web.Mvc.ViewEngineResult FindView(System.Web.Mvc.ControllerContext controllerContext, string viewName, string masterName, bool useCache);
        //    //  ReleaseView:释放当前控制器上下文ControllerContext中的视图。
        //    void ReleaseView(System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.IView view);
        //}

    我们将派生出它们的类,通过自定义视图引擎类实现渲染,并显示。
    准备资源:

    public class Student
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int Score { get; set; }
        }
    
        public class DataAccess
        {
            List<Student> students = new List<Student>();
    
            public DataAccess()
            {
                for (int i = 0; i < 10; i++)
                {
                    students.Add(new Student()
                    {
                        Id=i+1,
                        Name="Name"+Convert.ToString(i+1),
                        Score = i+80
                    });
                }
            }
    
            public List<Student> GetStudents()
            {
                return students;
            }
        }

    自定义扩展

    public class StudentView : IView
        {
            /// <summary>
            /// 渲染
            /// 通过获得视图上下文数据,然后自定义输出格式通过TextWriter输出数据
            /// </summary>
            /// <param name="viewContent"></param>
            /// <param name="writer"></param>
            public void Render(ViewContext viewContent, TextWriter writer)
            {
                //  从视图上下文ViewContext拿到model
                var model = viewContent.ViewData.Model;
                var students=model as List<Student>;
                //  自定义输出视图的html格式
                writer.Write("<table border=1><tr><th>编号</th><th>名称</th><th>分数</th></tr>");
                foreach (Student stu in students)
                {
                    writer.Write("<tr><td>" + stu.Id + "</td><td>" + stu.Name + "</td><td>" + stu.Score + "</td></tr>");
                }
                writer.Write("</table>");
            }
        }
    
        public class StudentViewEngine : IViewEngine
        {
            //呈现部分视图
            public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
            {
                throw new NotImplementedException();
            }
            //呈现视图
            public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
            {
                if (viewName == "StudentView")
                {
                    //  呈现自定义视图
                    return new ViewEngineResult(new StudentView(), this);
                }
                else
                {
                    return new ViewEngineResult(new string[] { "针对Student的视图还没创建!" });
                }
            }
            //显示视图
            public void ReleaseView(ControllerContext controllerContext, System.Web.Mvc.IView view)
            {
                
            }
        }

    至此自定义视图引擎完成。接下来我们进行调用:
    我们只需要在View中指定自定义视图的名称即可。

    public ActionResult Index()
            {
                var students = new DataAccess().GetStudents();
                ViewData.Model = students;
    
                return View("StudentView");
            }

    设置默认的,全局的视图引擎
    只需要添加ViewEngines.Engines.Add(new StudentViewEngine());即可实现。

    protected void Application_Start()
            {
                AreaRegistration.RegisterAllAreas();
    
                RegisterGlobalFilters(GlobalFilters.Filters);
                RegisterRoutes(RouteTable.Routes);
    
    
                ViewEngines.Engines.Add(new StudentViewEngine());
            }

    自定义HtmlHelper辅助方法

    定义扩展方法类和扩展方法

    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    
    //  设置命名空间为统一的Html命名空间
    namespace System.Web.WebPages.Html
    {
        //  设置静态类
        public static class HtmlExtensions
        {
            //  为HtmlHelper类提辅助方法Img,参数为src和alt
            public static MvcHtmlString Img(this HtmlHelper html,string src,string alt)
            {
                return MvcHtmlString.Create("<img src="" + src + "" alt="" + alt + "" />");
            }
    } }

    在View页面中调用该扩展方法

    @* 调用Img方法,前者为src,后者为alt,生成的结果是:<img src="C:imagestn.ico" alt="" /> *@
    @Html.Img("C:\images\btn.ico","")

    自定义Razor辅助方法

    在MVC项目根目录下新建一个文件夹名为:App_Code,用于存放MyHelpers.cshtml

    编写MyHelpers.cshtml内容如下:

    相当于定义了MyHelpers.li(List<string> arrays)方法,通过关键字helper进行声明,这样其他的cshtml页面都可以访问该方法

    @helper li(List<string> arrays) { 
        <ul>
            @foreach (var item in arrays)
            {
                <li>@(item)</li>
            }
        </ul>
    }

    cshtml页面访问li方法

    Index.cshtml页面调用:

    @MyHelpers.li(new List<string>() { "甲","乙","丙","丁"})

    页面输出结果:

    自定义AjaxHelper辅助方法

    与HtmlHelper扩展一样,为AjaxHelper扩展一个方法Textbox,并设置相应的属性

    public static class HtmlExtensions
        {
            //  为HtmlHelper类提辅助方法Img,参数为src和alt
            public static MvcHtmlString Img(this HtmlHelper html, string src, string alt)
            {
                return MvcHtmlString.Create("<img src="" + src + "" alt="" + alt + "" />");
            }
    
            public static MvcHtmlString Textbox(this AjaxHelper ajaxHelper, string name,  AjaxOptions ajaxOptions, object htmlAttributes)
            {
                //  设置标签名
                var tag = new TagBuilder("input");
                //  设置属性值
                tag.MergeAttribute("name", name);
                tag.MergeAttribute("type", "text");
    
                tag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
                tag.MergeAttributes((ajaxOptions ?? new AjaxOptions()).ToUnobtrusiveHtmlAttributes());
                tag.MergeAttribute("value", "自定义Ajax扩展");
                //  输出Html
                return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal));
            }
        }

    View调用:

    @Ajax.Textbox("search",
        new AjaxOptions
        {
            Url = @Url.Action("GetTime"),
            UpdateTargetId = "divTime",
            InsertionMode = InsertionMode.Replace
        },
    new { size = 50 })

    输出结果:
    输出结果的源码:

    <input data-ajax="true" data-ajax-mode="replace" data-ajax-update="#divTime" data-ajax-url="/AjaxHelperExt/GetTime" name="search" size="50" type="text" value="自定义Ajax扩展"></input>

    自定义UrlHelper辅助方法

    略.

    自定义控制器扩展BaseController

    //  BaseController  针对Controller进行重写
        //  并且提供了一些公用的方法如权限校验,Action跳转、日志记录等
        public class BaseController : Controller
        {
    
            protected override void OnException(ExceptionContext filterContext)
            {
                //  处理异常
                base.OnException(filterContext);
            }
    
            protected override void Initialize(RequestContext requestContext)
            {
                //  处理初始化信息,如Cookie,Session等缓存信息
                base.Initialize(requestContext);
            }
    
            protected override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                //  在调用操作方法前调用
                base.OnActionExecuting(filterContext);
            }
    
            protected override void OnActionExecuted(ActionExecutedContext filterContext)
            {
                //  在调用操作方法后调用
                base.OnActionExecuted(filterContext);
            }
    
            
        }

    自定义过滤器

    原文

    路由访问过滤器

    /// <summary>
        /// 路由访问过滤器
        /// </summary>
        public class SystemIActionFilter : IActionFilter
        {
            //  
            // Summary:  
            //     Called after the action method is invoked.  
            //      在Action返回之后  
            // Parameters:  
            //   filterContext:  
            //     Information about the current request and action.  
            public void OnActionExecuted(ActionExecutedContext filterContext)
            {
            }
            //  
            // Summary:  
            //     Called before the action method is invoked.  
            //      在进入Action之前  
            //      说明:使用RedirectToRouteResult进行路由值进行重定向时  
            //      RouteName 路由名称   
            //      RouteValues 路由值  特别注意第三个值 Permanent 获取一个值  
            //      该值指示重定向是否应为永久重定向 如果为true 在本程序会出现问题  
            // Parameters:  
            //   filterContext:  
            //     Information about the current request and action.  
            public void OnActionExecuting(ActionExecutingContext filterContext)
            {
                //验证 控制器 视图   
                string tempAction = filterContext.RouteData.Values["action"].ToString();
                string tempController = filterContext.RouteData.Values["controller"].ToString();
                string tempLoginAction = filterContext.RouteData.Values["action"].ToString();
    
                if (tempAction == "HomeLogin" && tempController == "Home" || tempLoginAction == "UserLogin" ? false : true)
                {
                    //请求登录时  
                    if (tempAction == "UserLogin" && tempController == "Home" ? false : true)
                    {
                        //Cookie  
                        HttpCookie tempToken = filterContext.HttpContext.Request.Cookies["exclusiveuser_token"];
                        if (tempToken == null)
                        {
                            filterContext.Result = new RedirectToRouteResult("HomeLogin", new RouteValueDictionary(new { controller = "Home", action = "HomeLogin" }), false);
                        }
                        //登录token不为null时  进行合法性验证token 头部,载荷,签名,cookie过期时间  
                        if (tempToken == null ? false : true)
                        {
                            //UserToken 方法 将验证 token 合法性 包括token 签名 ,token载荷,cookie 过期时间等  
                            //string SystemToken = new SecondTrackToken().UserToken();
                            //if (SystemToken == null)
                            //{
                            //    filterContext.Result = new RedirectToRouteResult("HomeLogin", new RouteValueDictionary(new { controller = "Home", action = "HomeLogin" }), false);
                            //};
                        }
                    }
                }
            }
        }
    路由访问过滤器

    异常处理过滤器

    /// <summary>
        /// 异常处理过滤器 
        /// </summary>
        public class SystemIExceptionFilter : IExceptionFilter
        {
            void IExceptionFilter.OnException(ExceptionContext filterContext)
            {
                Exception exception = filterContext.Exception;
                if (filterContext.ExceptionHandled)
                {
                    return;
                }
                HttpException http = new HttpException(null, exception);
                /*  
                 * filterContext.Exception.Message 错误信息 
                  */
                string messager = filterContext.Exception.Message;
    
                /*  
                 * 错误日志 
                  */
                //Log4NetHelp help = new Log4NetHelp();
                //help.ErrorString(filterContext.Exception.Message);
                /*  
                 * 设置自定义异常已经处理,避免其他过滤器异常覆盖 
                  */
                filterContext.ExceptionHandled = true;
    
                /*  
                 * 在派生类重写时,设置或者重写一个值该值指定是否禁用ISS7.0中自定义错误 
                  */
                filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
            }
        }
    异常处理过滤器

    授权处理(获取客户端信息)

    public class SystemIAuthorizationFilter : IAuthorizationFilter
        {
            void IAuthorizationFilter.OnAuthorization(AuthorizationContext filterContext)
            {
                //当前操作计算机用户   
                string pcName = ((System.Web.HttpServerUtilityWrapper)((System.Web.HttpContextWrapper)filterContext.RequestContext.HttpContext).Server).MachineName;
                //视图  
                string action = ((System.Web.Mvc.ReflectedActionDescriptor)filterContext.ActionDescriptor).ActionName;
                //控制器  
                string controller = ((System.Web.Mvc.ReflectedActionDescriptor)filterContext.ActionDescriptor).ControllerDescriptor.ControllerName;
                //请求时间  
                string time = filterContext.RequestContext.HttpContext.Timestamp.ToString();
                //请求相对路径  
                string absturl = ((System.Web.UnvalidatedRequestValuesWrapper)((System.Web.HttpRequestWrapper)((System.Web.HttpContextWrapper)filterContext.RequestContext.HttpContext).Request).Unvalidated).Url.AbsoluteUri;
                //状态  
                string code = ((System.Web.HttpResponseWrapper)((System.Web.HttpContextWrapper)filterContext.RequestContext.HttpContext).Response).Status;
                // 浏览器版本  
                string browser = ((System.Web.HttpBrowserCapabilitiesWrapper)((System.Web.HttpRequestWrapper)((System.Web.HttpContextWrapper)filterContext.RequestContext.HttpContext).Request).Browser).Type;
                //请求方式  
                string gepPost = ((System.Web.HttpRequestWrapper)((System.Web.Mvc.Controller)filterContext.Controller).Request).RequestType;
                //本地主机名称解析DNS本身处理。  
                string server = ((System.Web.HttpRequestWrapper)((System.Web.HttpContextWrapper)filterContext.HttpContext).Request).UserHostAddress;
                #region  server 说明  
                /* 
                  * 版权(c)1993 - 2009微软(msft . o:行情)。 
                  * 
                  * 这是一个示例所使用的主机文件微软为Windows TCP / IP。 
                  * 
                  * 这个文件包含IP地址到主机名的映射。 
                              每一个 
                  * 条目应该保存在单个行。 
                  IP地址应 
                  *被放置在第一列对应的主机名。 
                  *的IP地址和主机名应该由至少一个 
                  *空间。 
                  * 
                  *此外,评论(这样的)可能是插入的个人 
                  *线或后机器名称用“*”符号。 
                  * 
                  例如: 
                  * 
                  * 102.54.94.97 rhino.acme.com源服务器 
                  * 38.25.63.10 x.acme.com x客户机主机 
    
                  *本地主机名称解析DNS本身处理。 
                  * 127.0.0.1 localhost 
                  *::1 localhost 
                  */
                #endregion
                //用户  
                //部门  
                //职位  
    
            }
        }
    授权处理过滤器

    自定义属性过滤器

    public class CheckLogin: ActionFilterAttribute
        {
            public override void OnResultExecuting(ResultExecutingContext filterContext)
            {
                HttpCookieCollection CookieCollect = System.Web.HttpContext.Current.Request.Cookies;
                if (CookieCollect["username"] == null || CookieCollect["password"] == null)
                {
                    filterContext.Result = new RedirectResult("/Home/Login");
                }
                else
                {
                    if (CookieCollect["username"].Value != "admin" && CookieCollect["password"].Value != "123456")
                    {
                        filterContext.Result = new RedirectResult("/Home/Login");
                    }
                }
            }
        }
    检查登录的过滤器

    过滤器定义好后,需要在过滤器配置类FilterConfig中添加

    public class FilterConfig
        {
            public static void RegisterGlobalFilters(GlobalFilterCollection filters)
            {
                filters.Add(new HandleErrorAttribute());
    
                //将自定义异常过滤器的优先级提高,防止异常被默认的HandleError处理(也可以自定义类重写HandleErrorAttribute 实现错误处理)  
                filters.Add(new SystemIExceptionFilter(), 1);
                //控制器过滤器  
                filters.Add(new SystemIActionFilter(), 2);
                //授权过滤器  
                filters.Add(new SystemIAuthorizationFilter(), 3);
            //  自定义属性过滤器
                filters.Add(new CheckLogin());
    } }

    自定义属性过滤器在控制器中调用:
    在方法的上面加上特性:CheckLogin,当调用该方法时会先进行过滤再执行下面的逻辑。

     [CheckLogin]
            public ActionResult About()
            {
                
                ViewBag.Message = "Your application description page.";
    
                return View();
            }

    自定义ActionResult

    扩展的ActionResult,继承自ActionResult,需要重写ExecuteResult方法,通过构造函数传入参数值,

    使用ExecuteResult方法的ControllerContext上下文获得HttpResponse,HttpResponse使用输出参数值结果。

     /// <summary>
        /// 自定义JObject返回结果
        /// </summary>
        public class JObjectActionResult : ActionResult
        {
            /// <summary>
            /// 结果集
            /// </summary>
            public JObject JObject
            {
                get;
                set;
            }
    
            public Encoding ContentEncoding
            {
                get;
                set;
            }
    
            public override void ExecuteResult(ControllerContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException("context");
                }
                HttpResponseBase response = context.HttpContext.Response;
                response.ContentType = "application/json";
                if (ContentEncoding != null)
                {
                    response.ContentEncoding = ContentEncoding;
                }
                if (JObject != null)
                {
                    response.Write(JObject.ToString());
                }
            }
        }
    JObjectActionResult

    通过response.Write(JObject.ToString());最后输出结果。
    接下来我们看看调用:

    public ActionResult Index()
            {       
                return View();
            }

    默认的View()是Controller.View()的方法,因此我们为Controller类扩展一个方法。定义扩展内容如下:

    public static class JObjectActionResultExtensions
        {
            public static JObjectActionResult JObjectResult(this Controller controller, JObject obj)
            {
                return new JObjectActionResult { JObject = obj };
            }
    
            public static JObjectActionResult JObjectResult(this Controller controller, bool success)
            {
                JObject obj = new JObject();
                obj.Add("success", success);
                if (success)
                {
                    obj.Add("code", 200);
                    obj.Add("msg", "Success!");
                }
                else
                {
                    obj.Add("code", 500);
                    obj.Add("msg", "Error!");
                }
                return JObjectResult(controller, obj);
            }
    
            public static JObjectActionResult JObjectResult(this Controller controller, bool success, int code, string msg)
            {
                JObject obj = new JObject();
                obj.Add("success", success);
                obj.Add("code", code);
                obj.Add("msg", msg);
                return JObjectResult(controller, obj);
            }
        }
    JObjectActionResult的扩展

    控制器中调用输出:
    使用this关键字调用JObjectResult即可输出结果。

    public ActionResult About()
            {            
                ViewBag.Message = "Your application description page.";
                return this.JObjectResult(true);
            }

    接下来我们再扩展一个序列化的Result.

    /// <summary>
        /// 泛型的序列化结果
        /// </summary>
        /// <typeparam name="TData"></typeparam>
        public class CustomView<TData>  : ActionResult where TData:class,new()
        {
            /// <summary>
            /// 构造函数传入参数
            /// </summary>
            /// <param name="t"></param>
            public CustomView(TData t) { data = t; }
            public TData data;
    
            protected JsonSerializerSettings SerializerSettings;
    
            protected void InitSerialization(ControllerContext context)
            {
                HttpResponseBase response = context.HttpContext.Response;
                response.ContentType = "text/html";
                if (SerializerSettings == null)
                {
                    SetSerializerSettings();
                }
                response.Write(JsonConvert.SerializeObject(data, Formatting.None, SerializerSettings));
            }
    
            protected virtual void SetSerializerSettings()
            {
                SerializerSettings = new JsonSerializerSettings
                {
                    Converters = new List<JsonConverter>
                    {
                        new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd hh:mm" }
                    }
                };
            }
    
            public override void ExecuteResult(ControllerContext context)
            {
                InitSerialization(context);
            }
        }
    CustomView

    调用它,只需要实例化一下就行了。

    public ActionResult Contact()
            {
                ViewBag.Message = "Your contact page.";
                User u = new Models.User();
                return  new CustomView<User>(u);
            }

     下载地址

    大家还有什么好的扩展方法,可以回复该帖哦。

  • 相关阅读:
    js联系题目
    js运算符
    太极图
    第一周 Welcome
    对 vscode 自动格式化的结果不太满意,我们该如何自己调整直至自己满意为止
    ASP.NET MVC5.0 OutputCache不起效果
    对照实验(1)-批量清理系统临时文件
    ES6
    19.局部变量和全局变量
    18.函数定义和参数
  • 原文地址:https://www.cnblogs.com/licin/p/8459778.html
Copyright © 2011-2022 走看看