CRUD全栈式编程架构之控制器的设计

页面

这里界面我采用jquery miniui来做的,当你完全了解了整个设计之后可以轻松切换到其他的js框架,个人认为类似muniui,easyui等等这类可以将web界面做得和winform类似的框架,特别适合做后台管理系统。要讨论controller的设计必须结合界面,这里我给出界面截图和控制器的代码,这一篇主要讲控制器的代码,下一篇再讲界面的设计。

上一篇忘记说了,IVeiwModel是一个dto或者说viewmode的接口,我的应用里面一般不严格区分viewmode和dto,这个接口之后一个long Id的属性,

代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Routing;
using Coralcode.Framework.Extensions;
using Coralcode.Framework.Log;
using Coralcode.Framework.Models;
using Coralcode.Framework.Mvc.Extensions;
using Coralcode.Framework.Mvc.Models.MiniUI;
using Coralcode.Framework.Mvc.Template;
using Coralcode.Framework.Services;
using Coralcode.Framework.Validator;
using Newtonsoft.Json;
using ViewType = Coralcode.Framework.Mvc.Models.MiniUI.ViewType;

namespace Coralcode.Framework.Mvc.ControlContent
{
    public abstract class CrudCoralController<TModel, TSearch, TOrder> : ContextCoralController
        where TModel : class, IViewModel, new()
        where TSearch : SearchBase, new()
        where TOrder : OrderBase, new()
    {
        private readonly ICrudCoralService<TModel, TSearch, TOrder> _service;
        protected CrudCoralController(ICrudCoralService<TModel, TSearch, TOrder> service)
        {
            _service = service;
        }

        protected override void Initialize(RequestContext requestContext)
        {
            base.Initialize(requestContext);
            var routeValues = Request.GetRouteValues();
            //页面的配置
            ViewBag.Title = GetType().GetDescription();
            ViewBag.EditUrl = Url.Action("AddEdit", routeValues);
            ViewBag.DeleteUrl = Url.Action("BatchDelete", routeValues);
            ViewBag.ListUrl = Url.Action("List", routeValues);
            ViewBag.PageUrl = Url.Action("PageSearch", Request.GetRouteValues());
            ViewBag.ShowPager = true;
        }

        [HttpGet]
        public virtual ActionResult Index()
        {
            var viewModelType = typeof(TModel);
            var properties =
                viewModelType.GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            var columns = new List<DataGridColumn>();
            foreach (var propertyInfo in properties)
            {
                var descAttr = propertyInfo.GetCustomAttribute<GridColumnAttribute>();
                if (descAttr == null) continue;

                var column = descAttr.DataGridColumn;

                if ((column.ViewType & ViewType.List) == 0)
                {
                    continue;
                }

                if (string.IsNullOrWhiteSpace(column.Field))
                {
                    column.Field = propertyInfo.Name;
                }

                columns.Add(column);

                var dateType = propertyInfo.GetCustomAttribute<DataTypeAttribute>();

                if (dateType == null)
                    continue;
                switch (dateType.DataType)
                {
                    case DataType.Custom:
                        break;
                    case DataType.DateTime:
                        column.Renderer = "onDateTimeRenderer";
                        break;
                    case DataType.Date:
                        column.Renderer = "onDateRenderer";
                        break;
                    case DataType.Time:
                        column.Renderer = "onTimeRenderer";
                        break;
                    case DataType.Duration:
                        break;
                    case DataType.PhoneNumber:
                        break;
                    case DataType.Currency:
                        break;
                    case DataType.Text:
                        break;
                    case DataType.Html:
                        break;
                    case DataType.MultilineText:
                        break;
                    case DataType.EmailAddress:
                        break;
                    case DataType.Password:
                        break;
                    case DataType.Url:
                        break;
                    case DataType.ImageUrl:
                        break;
                    case DataType.CreditCard:
                        break;
                    case DataType.PostalCode:
                        break;
                    case DataType.Upload:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
            ViewBag.Header = columns;

            ListBindData();

            return View(new TSearch());
        }

        [HttpPost]
        public virtual JsonResult List(TSearch search)
        {
            return ToJson(_service.Search(search));
        }

        [HttpPost]
        public virtual JsonResult PageSearch(TSearch search,PageInfo page,TOrder order)
        {
            return ToJson(_service.PageSearch(page,search, order));
        }

        /// <summary>
        /// 这里用来做编辑
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet]
        public virtual ActionResult AddEdit(long? id)
        {
            TModel model = id.HasValue ? _service.Get(id.Value) : new TModel();
            AddEditBindData(model);
            return View(model);
        }

        [ValidateInput(false)]
        [HttpPost]
        public virtual JsonResult AddEdit(TModel model)
        {
            try
            {
                if (!ModelState.IsValid)
                    return AjaxErrorResult(ModelState.GetErroreMessage());
                var ajaxMessage = ValidateAndPreProccess(model);
                if (ajaxMessage.State != ResultState.Success)
                {
                    return AjaxErrorResult(ajaxMessage.Message);
                }

                if (model.Id < 1)
                    _service.Create(model);
                else
                    _service.Modify(model);
            }
            catch (Exception ex)
            {
                LoggerFactory.Instance.Error("{0}编辑产生错误;数据:{1}", ex, typeof(TModel),
                    JsonConvert.SerializeObject(model ?? new TModel()));
                return AjaxExceptionResult(ex);
            }
            return AjaxOkResult();
        }

        /// <summary>
        /// 业务验证
        /// 比如用户名唯一
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected virtual ResultMessage ValidateAndPreProccess(TModel model)
        {
            if (!EntityValidatorProvider.Validator.IsValid(model))
                return new ResultMessage
                {
                    State = ResultState.Fail,
                    Message = string.Join(";<br />", EntityValidatorProvider.Validator.GetInvalidMessages(model))
                };

            return new ResultMessage
            {
                State = ResultState.Success
            };
        }

        protected virtual void ListBindData()
        {

        }

        /// <summary>
        /// 这里用来扩展绑定数据
        /// </summary>
        protected virtual void AddEditBindData(TModel model)
        {
            //这里用来做数据绑定的操作
        }

        [HttpPost]
        public virtual JsonResult Delete(long id)
        {
            return this.BatchDelete(id.ToString());
        }

        [HttpPost]
        public virtual JsonResult BatchDelete(string ids)
        {
            if (string.IsNullOrWhiteSpace(ids)) return AjaxErrorResult("参数不能为空");
            try
            {
                var idList = ids.Split(‘,‘).ToList().ConvertAll(Convert.ToInt64);
                if (idList.Count < 1) return AjaxErrorResult("参数不能为空");
                _service.Remove(idList);
            }
            catch (Exception ex)
            {
                LoggerFactory.Instance.Error("{0}编辑产生错误;数据:{1}", ex, typeof(TModel), JsonConvert.SerializeObject(ids));
                return AjaxExceptionResult(ex);
            }
            return AjaxOkResult();
        }
    }

    public abstract class CrudCoralController<TModel, TSearch> : CrudCoralController<TModel, TSearch, OrderBase>
        where TModel : class, IViewModel, new()
        where TSearch : SearchBase, new()
    {
        protected CrudCoralController(ICrudCoralService<TModel, TSearch, OrderBase> service) : base(service)
        {
        }
    }

    public abstract class CrudCoralController<TModel> : CrudCoralController<TModel, SearchBase, OrderBase>
        where TModel : class, IViewModel, new()
    {
        protected CrudCoralController(ICrudCoralService<TModel, SearchBase, OrderBase> service)
            : base(service)
        {
        }
    }

}

操作

页面配置主要是在Controler.Initialize 方法中配置的。这里有个地方注意,在生成url的地方一定要带上routedata,这个可以充分利用mvc自带valueprovider的设计,结合菜单url可以为查询和模型的绑定提供值绑定。这部分会在完整demo放出后再详细说明。

新增/编辑

这里我把新增和编辑作为一个来对待,大部分情况都是这样,当id为0时候认为是新增,当id不为0的时候是编辑。当然如果你想分开只需添加一个配置和一个方法并结合界面js,
扩展必须是允许的

删除

这里我只使用了批量删除的方法,界面上应该给出的是checkbox,选择之后点击删除即可.这里注意界面传输过来的数据是“,”分开的字符串,解析之后做删除操作。

查询

查询提供了未分页和分页两种。分页的话搭配showpager来配置。大部分后台列表都需要分页,但是如果结合三级菜单,结合url中routedata,可以将数据进行一定划分,数据量不大的情况下不用分页用list也一样方便。这里界面上我没有排序的功能,后面我整理demo的时候再给出排序吧。

列表

数据绑定

在index方法中我们首先取出模型的元数据,那些列需要绑定到界面,数据类型是什么,并且可以定义一些一些元。例如高度,宽度,当然最好是搭配界面自适应使用。另外给出listbindata这个没有实现的方法,主要是给查询时候绑定数据用的。例如你查询实体中有类型combox,可以获取到类型用ViewBag传递到界面使用。另外还可以给界面一些默认值,例如你查询实体里面有开始时间,结束时间,也可以在这里给出配置。当然如果你查询实体只是独享只提供一个表查询,那么最好是写在构造函数中。

分页

分页和查询只多了一个pageinfo(里面只包含当前页和行数)
ps:mvc的绑定机制会会导致无法赋值,或者复制错乱问题
>* pageinfo的属性名称不要和查询实体的属性名称冲突,除非是业务需要。 
>* 另外一定不要再查询实体中定义类似page,search,order属性名。
>* 如area,controller,action的作为入参或者入参的属性也不要有

新增和编辑

区分和绑定

编辑通过id是否有值来区分,如果有值,那么会使用服务查询到数据绑定到界面,没有的话会new一个出来。AddEditBindData 方法是为页面绑定提供数据源,例如编辑界面有combox这类可以更方便绑定。有人会说可以用界面ajax无刷新绑定,我建议是尽量不要这么做,如果界面有几十个combox这里很容易导致大量的ajax请求,我称之为ajax灾难,所以我这里会强烈建议使用者使用后台提供数据绑定,避免上述问题。

验证和预处理

提交数据之后首先做的事mvc自带的验证,这里做了一个小封装,可以取出所有错误返回到界面,不过最佳方式是,界面mvc客户端js做绑定工作。具体可以查看artechmvc讲解中的方式。另外这里做了自定义的验证。这里把方法名叫ValidateAndPreProccess,因为有时候我们需要对传输过来的数据做一部分预处理,比如ids这种有可能你服务需要的是一个list,这时候就可以做一个转换了。另外验证和预处理逻辑很难区分出是那个先做哪个后做,所以这里将两个方法合并。预处理中调用了自定义验证,这里用的NlayerApp的验证方式,具体请自行搜索,其实这里并没有卵用。mvc的验证比这个做得好,但是后面做导入导出的时候会用到暂且就放这里吧,结合导入导出再说这部分。

  public static class MvcExtensions
    {
        public static string GetErroreMessage(this ModelStateDictionary state)
        {
           return string.Join( Environment.NewLine,state.SelectMany(item => item.Value.Errors)
                .Select(item => item.ErrorMessage)
                .Where(item => !string.IsNullOrWhiteSpace(item)));
        }
    }

其他

AjaxXXXResult

分为成功,失败和部分成功,部分成功会在导入数据时候用到。在扩展控制器的顶层基类提供更多的方法也是做应用时候必须的,就算你啥都没有也建议你先占个位置.

using System;
using System.Web.Mvc;
using Coralcode.Framework.Models;
using Coralcode.Framework.Mvc.ActionResults;

namespace Coralcode.Framework.Mvc.ControlContent
{
    public class CoralController : Controller
    {
        protected JsonResult AjaxOkResult(object data = null, string message = "success")
        {
            var result = new ResultMessage
            {
                State = ResultState.Success,
                Message = message,
                Data = data,
            };
            return ToJson(result, JsonRequestBehavior.AllowGet);
        }
        protected JsonResult AjaxExceptionResult(Exception ex, object data = null)
        {
            var result = new ResultMessage
            {
                State = ResultState.Fail,
                Message = ex.ToString(),
                Data = data,
            };

            return ToJson(result, JsonRequestBehavior.AllowGet);
        }
        protected JsonResult AjaxErrorResult(object data = null, string message = "fail")
        {
            var result = new ResultMessage
            {
                State = ResultState.Fail,
                Message = message,
                Data = data,
            };
            return ToJson(result, JsonRequestBehavior.AllowGet);
        }
        protected JsonResult AjaxPartSuccessResult(object data = null, string message = "partsuccess")
        {
            var result = new ResultMessage
            {
                State = ResultState.PartSuccess,
                Message = message,
                Data = data,
            };

            return ToJson(result, JsonRequestBehavior.AllowGet);
        }
        protected JsonResult ToJson(object obj, JsonRequestBehavior behavior = JsonRequestBehavior.AllowGet)
        {
            return new CustomJsonResult()
            {
                Data = obj,
                JsonRequestBehavior = behavior
            };

        }

    }
}

ResultMessage

这里泛型,主要是用在服务端调用api时候数据转换类型更方便,所有的我ajax请求都做了类似封装,这里会在和mvc结合时候重点说明。

namespace Coralcode.Framework.Models
{
    public class BaseMessage
    {
        public BaseMessage() { }
        public BaseMessage(ResultState state,string  message="" )
        {
            State = state;
            Message = message;
        }

        public ResultState State { get; set; }
        public string Message { get; set; }
    }

    public class ResultMessage : BaseMessage
    {
        public ResultMessage() { }
        public ResultMessage(ResultState state, string message = "",object data=null)
            : base(state, message)
        {
            Data = data;
        }
        public object Data { get; set; }

    }

    public class ResultMessage<T> : BaseMessage
    {
        public ResultMessage() { }
        public T Data { get; set; }

        public ResultMessage(ResultState state, string message = "",T data=default(T))
            : base(state, message)
        {
            Data = data;
        }
    }

    public enum ResultState
    {
        Success,
        Fail,
        PartSuccess,
    }
}

CustomJsonResult

这里对JsonResult 做了扩展,性能更快,并且解决long类型数据返回到界面数据最后两位丢失的问题。

using System;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Coralcode.Framework.Mvc.ActionResults
{
    public class CustomJsonResult : JsonResult
    {

        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
                String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException();
            }

            HttpResponseBase response = context.HttpContext.Response;

            if (!String.IsNullOrEmpty(ContentType))
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "application/json";
            }
            if (ContentEncoding != null)
            {
                response.ContentEncoding = ContentEncoding;
            }
            if (Data != null)
            {
                response.Write(JsonConvert.SerializeObject(Data, new IdToStringConverter()));
            }
        }
    }

    public class IdToStringConverter : JsonConverter
    {
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JToken jt = JValue.ReadFrom(reader);

            return jt.Value<long>();
        }

        public override bool CanConvert(Type objectType)
        {
            return typeof(Int64) == objectType;
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, value.ToString());
        }
    }

}

PS:
  这个js精度丢失的问题是由于js本身设计时候一个bug。这里刚开始遇到也困扰很久,一直用类中一个字符串属性取代。但是后来狠下心,还是从mvc底层去解决问题。有问题不要绕,就是干不要怂-_-!

  这里注意JsonRequestBehavior.AllowGet ,面试提问利器-_-!

PS:注意我所有的ps,都是踩过的坑,并且是不容易发现的坑。

  

时间: 2024-08-08 05:35:45

CRUD全栈式编程架构之控制器的设计的相关文章

CRUD全栈式编程架构之界面层的设计

Layout的设计 模板模式 mvc的模板特别类似设计模式中模板方法模式,结合Layout中RenderSection和RenderBody方法可以将部分html展现逻辑延迟到具体的视图页面去实现里面实现.结合我们增删改查的逻辑,我们的用户界面,我们将页面分为这几个区域,实现部分逻辑以后,部分留给具体的页面去实现.例如图片中新增,编辑,删除,导入,导出,查询都是架构自带的操作,至于复制就给页面扩展,查询条件也留给具体的页面中扩展,模板中给出RenderSection即可. 执行顺序 这个执行顺序

CRUD全栈式编程架构之服务层的设计

服务层代码 首先我先放出2个主要类的代码再分别讲解 接口 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Coralcode.Framework.Models; using Coralcode.Framework.Page; namespace Coralcode.Framework.Services

CRUD全栈式编程架构之MVC的扩展设计

MVC执行流程 路由的扩展 我理解的路由作用有以下几个 Seo优化,用“/”分开的url爬虫更爱吃 物理和逻辑文件分离,url不再按照文件路径映射 Controller,Action的选择 MVC路由的扩展 实话实说MVC的路由我很少去做扩展,在MVC4时代,还会去重写掉url的大小写,而在MVC5之后,MVC自带了配置去小写化url.不过有一个配置还是必须要提一下那就是Area,在你的系统达到一定规模之后,Controllers通过Area来管理将会变得更容易.这里给出我的Area扩展,很简单

数据层全栈式编程架构

CRUD全栈式编程架构之数据层的设计 CodeFirst 一直以来我们写应用的时候首先都是创建数据库 终于在orm支持codefirst之后,我们可以先建模. 通过模型去创建数据库,并且基于codefirst可以实现方便的 实现数据库迁移的工作.使用codefirst有以下几个技巧, 以EntityFramework为例,结合我这个设计做了以下改进 1.模型的识别 建立一个基类命名Entity,里面只有一个long类型的id字段. 所有需要映射到数据库的模型都继承自Entity, + 2.模型的

CRUD全栈式编程概述

业务场景 CRUD,从数据驱动的角度几乎所有的的业务都是在做这样的事情.  几乎所有的操作都是在做对表的增删该查.  假设我们将数据库数据规个类:  分为基础/配置数据和业务/增长数据,或者说静态数据和动态数据.  其中静态数据是由后台管理员编辑的产生,动态数据是由客户产生.  那么这部分中的静态数据往往伴随着完整的增删改查逻辑.  完整的增删改查逻辑指的是,有对数据库某个表数据的查询.  一条或者几条数据的添加,删除,修改.  再直白一点就是有个界面,上面有查询,添加,删除,修改,导入,导出的

基于NodeJS的全栈式开发

随着不同终端(Pad/Mobile/PC)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本.为了提升开发效率,前后端分离的需求越来越被重视,后端负责业务/数据接口,前端负责展现/交互逻辑,同一份数据接口,我们可以定制开发多个版本. 这个话题最近被讨论得比较多,阿里有些BU也在进行一些尝试.讨论了很久之后,我们团队决定探索一套基于NodeJS的前后端分离方案,过程中有一些不断变化的认识以及思考,记录在这里,也希望看到的同学参

如何学习(1):构建全栈式知识结构

有次下班到家楼下等电梯,碰巧一位妈妈抱到两岁的小女孩在看旁边的宣传画.这时电梯还没到,这位妈妈就指着海报上的字读给小女孩,"这是太阳,那是月亮"--,想借这个机会教小孩认字. 这是中国式的.传统的教学方法,其实我对这种死记硬背的方法不怀好意,于是在电梯上开起了小差,为什么这种方法效果不好,不招受教者的讨好呢. 如果我是教自己的小女儿认字,我会怎么教呢? "牛牛,你看,上面画的是太阳.你知道吗?太阳公公每天很早就起床了,大地才开始暖起来,小朋友们才可以出来玩耍.到了晚上,太阳公

也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

随着不同终端(Pad/Mobile/PC)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本.为了提升开发效率,前后端分离的需求越来越被重视,后端负责业务/数据接口,前端负责展现/交互逻辑,同一份数据接口,我们可以定制开发多个版本. 这个话题最近被讨论得比较多,阿里有些BU也在进行一些尝试.讨论了很久之后,我们团队决定探索一套基于NodeJS的前后端分离方案,过程中有一些不断变化的认识以及思考,记录在这里,也希望看到的同学参

基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离) 前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们重新思考了“前后端”的定义,引入前端同学都熟悉的NodeJS,试图探索一条全新的前后端分离模式. 随着不同终端(Pad/Mobile/PC)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本.为了提升开发效率,前后端分离的需求越