SSM框架整合项目 :租房管理系统 - 谁将新樽辞旧月,今月曾经照古人 - 博客园

mikel阅读(1768)

来源: SSM框架整合项目 :租房管理系统 – 谁将新樽辞旧月,今月曾经照古人 – 博客园

使用ssm框架整合,oracle数据库

框架:

Spring

SpringMVC

MyBatis

导包:

1, spring

2, MyBatis

3, mybatis-spring

4, fastjson

5, aspectweaver—-AspectJ框架

6, log4j—–打印日志信息

7, ojdbc6.jar

8, jstl.jar, standard.jar—-标准标签库

9, commons-logging-1.2.jar

10,……

项目结构:

配置文件同前面:http://www.cnblogs.com/jiangwz/p/7674275.html

项目代码:

model:

复制代码
 1 package com.hanqi.model;
 2 
 3 public class House {
 4     
 5     private Integer id;
 6     private String keyword;
 7     private String area;
 8     private Integer squaremeter;
 9     private Integer rent;
10     private String renttype;
11     private String housetype;
12 
13     public House(Integer id, String keyword, String area, Integer squaremeter, Integer rent, String renttype,
14             String housetype) {
15         super();
16         this.id = id;
17         this.keyword = keyword;
18         this.area = area;
19         this.squaremeter = squaremeter;
20         this.rent = rent;
21         this.renttype = renttype;
22         this.housetype = housetype;
23     }
24 
25     public House() {
26         super();
27         // TODO Auto-generated constructor stub
28     }
29 
30     public Integer getId() {
31         return id;
32     }
33 
34     public void setId(Integer id) {
35         this.id = id;
36     }
37 
38     public String getKeyword() {
39         return keyword;
40     }
41 
42     public void setKeyword(String keyword) {
43         this.keyword = keyword;
44     }
45 
46     public String getArea() {
47         return area;
48     }
49 
50     public void setArea(String area) {
51         this.area = area;
52     }
53 
54     public Integer getSquaremeter() {
55         return squaremeter;
56     }
57 
58     public void setSquaremeter(Integer squaremeter) {
59         this.squaremeter = squaremeter;
60     }
61 
62     public Integer getRent() {
63         return rent;
64     }
65 
66     public void setRent(Integer rent) {
67         this.rent = rent;
68     }
69 
70     public String getRenttype() {
71         return renttype;
72     }
73 
74     public void setRenttype(String renttype) {
75         this.renttype = renttype;
76     }
77 
78     public String getHousetype() {
79         return housetype;
80     }
81 
82     public void setHousetype(String housetype) {
83         this.housetype = housetype;
84     }
85 
86     @Override
87     public String toString() {
88         return "House [id=" + id + ", keyword=" + keyword + ", area=" + area + ", SQUAREMETER=" + squaremeter
89                 + ", rent=" + rent + ", renttype=" + renttype + ", housetype=" + housetype + "]";
90     }
91 
92 }
复制代码

dao层:

复制代码
 1 package com.hanqi.dao;
 2 
 3 import java.util.List;
 4 
 5 import com.hanqi.model.House;
 6 
 7 public interface HouseDao {
 8 
 9     List<House> selectAll();
10     
11     int inserthouse(House house);
12 
13     int delhouse(Integer id);
14 
15     int updatehouse(House house);
16 
17     List<House> selectinfo(House house);
18 
19 }
复制代码

dao层实现方法:

复制代码
 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE mapper
 3   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 4   "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 5 <mapper namespace="com.hanqi.dao.HouseDao">
 6 
 7     <select id="selectAll" resultType="House">
 8         select t.*from TABLE_HOUSE t
 9     </select>
10     
11     <insert id="inserthouse" parameterType="House" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
12         insert into TABLE_HOUSE values(test1.nextval,#{keyword},#{area},#{squaremeter},#{rent},#{renttype},#{housetype})
13     </insert>
14     
15     <delete id="delhouse" parameterType="Map">
16         delete TABLE_HOUSE t where t.id=#{id}
17     </delete>
18     
19     <update id="updatehouse" parameterType="Map">
20         update TABLE_HOUSE t set t.keyword=#{keyword},t.area=#{area},t.squaremeter=#{squaremeter},t.rent=#{rent},t.renttype=#{renttype},t.housetype=#{housetype} where t.id=#{id}
21     </update>
22     
23     <select id="selectinfo" resultType="House" parameterType="House">
24         select t.* from TABLE_HOUSE t
25         <where>
26             <if test="keyword!=null">
27                 and t.keyword like #{keyword}
28             </if>
29             <if test="area!=null">
30                 and t.area=#{area}
31             </if>
32             <if test="renttype!=null">
33                 and t.renttype in #{renttype}
34             </if>
35             <if test="housetype!=null">
36                 and t.housetype=#{housetype}
37             </if>
38         </where>
39         
40     </select>
41 </mapper>
复制代码

controller控制器:

复制代码
 1 package com.hanqi.controller;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.stereotype.Controller;
 8 import org.springframework.ui.Model;
 9 import org.springframework.web.bind.annotation.RequestMapping;
10 import org.springframework.web.bind.annotation.ResponseBody;
11 import org.springframework.web.bind.annotation.SessionAttributes;
12 import org.springframework.web.servlet.ModelAndView;
13 
14 import com.alibaba.fastjson.JSONObject;
15 import com.hanqi.dao.HouseDao;
16 import com.hanqi.model.House;
17 
18 @Controller
19 @SessionAttributes("currentUser")
20 @RequestMapping("/house")
21 public class HouseController {
22 
23     @Autowired
24     private HouseDao houseDao;
25     
26     @ResponseBody
27     @RequestMapping("/selectAll")
28     public JSONObject selectAll() {
29         JSONObject jo = new JSONObject();
30         List<House> list = houseDao.selectAll();
31         jo.put("total", list.size());
32         jo.put("rows", list);
33         
34         return jo;
35     }
36     
37     @ResponseBody
38     @RequestMapping("/selectinfo")
39     public ModelAndView  selectinfo(House house){
40         
41         house.setKeyword("%"+house.getKeyword()+"%");
42         List<House> list = houseDao.selectinfo(house);
43         ModelAndView modelAndView = new ModelAndView();
44         modelAndView.setViewName("houselook3");
45         modelAndView.addObject("list",list);
46 
47         return modelAndView;
48         
49     }
50     
51     @ResponseBody
52     @RequestMapping("/selectAll1")
53     public ModelAndView selectAll1(Model model) {
54         //List<House> list = houseDao.selectAll();
55         //System.out.println(list);
56         
57         //model.addAttribute("list", list);
58 
59         ModelAndView mv = new ModelAndView("redirect:/houselook.jsp");
60         return mv;
61         
62     }
63 
64     @RequestMapping("/selectAll2")
65     public String selectAll2(Model model) {
66         List<House> list = houseDao.selectAll();
67         System.out.println(list);
68         model.addAttribute("list", list);
69 
70         return "houselook2";
71     }
72     
73     @ResponseBody
74     @RequestMapping("/addhouse")
75     public ModelAndView addhouse(House house) {
76         int i=houseDao.inserthouse(house);
77         
78         ModelAndView mv = new ModelAndView("redirect:/index.jsp");
79         return mv;
80         
81     }
82     
83     @ResponseBody
84     @RequestMapping("/delhouse")
85     public ModelAndView delhouse(Integer id) {
86         int i=houseDao.delhouse(id);
87         ModelAndView mv = new ModelAndView("redirect:/index.jsp");
88         return mv;
89     }
90     
91     @ResponseBody
92     @RequestMapping("/updatehouse")
93     public ModelAndView updatehouse(House house) {
94         int i=houseDao.updatehouse(house);
95         ModelAndView mv = new ModelAndView("redirect:/index.jsp");
96         return mv;
97     }
98     
99 }
复制代码

前台:

复制代码
  1 <%@ page language="java" contentType="text/html; charset=UTF-8"
  2     pageEncoding="UTF-8"
  3     %>
  4 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
  5 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  6 <html>
  7 <head>
  8 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  9 <script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
 10 <script type="text/javascript"
 11     src="jquery-easyui-1.5.1/jquery.easyui.min.js"></script>
 12     <link rel="shortcut icon" href="img/logo1.jpg"/>
 13 <link type="text/css" rel="stylesheet"
 14     href="jquery-easyui-1.5.1/themes/icon.css"></link>
 15 <link type="text/css" rel="stylesheet"
 16     href="jquery-easyui-1.5.1/themes/default/easyui.css"></link>
 17 <script type="text/javascript"
 18     src="jquery-easyui-1.5.1/locale/easyui-lang-zh_CN.js"></script>
 19 <title>租房管理</title>
 20 <%
 21     String basePath = request.getContextPath();
 22 %>
 23 <!-- <script type="text/javascript" src="js/index.js"></script> -->
 24 <style type="text/css">
 25 .datagrid-btable tr {
 26     height: 30px;
 27 }
 28 </style>
 29 </head>
 30 
 31 <body class="easyui-layout">
 32     <!-- 添加商品 -->
 33     <div data-options="region:'north',split:true"
 34         style="height: 50px; background-color: cornflowerblue">
 35         
 36     </div>
 37     <!-- 对话框开始 -->
 38     <div data-options="region:'center',split:true"
 39         style="padding: 5px; background: #eee">
 40         <div id="tabs" class="easyui-tabs" style="width: 100%; height: 100%;">
 41             <div title="主页" style="">
 42                 <table id="table"></table>
 43                 <!-- 添加的表单 -->
 44                 <div id="zhong" style="display: none">
 45                     <form id="addhouse" method="post"
 46                         style="width: 600px; padding: 20px">
 47                         关键字:<input type="text" name="keyword"><br>
 48                         地区:<input type="text" name="area"><br>
 49                         面积:<input type="text" name="squaremeter"><br>
 50                         租金:<input type="text" name="rent"><br>
 51                         租赁方式:<input type="text" name="renttype"><br>
 52                         房屋类型:<input type="text" name="housetype"><br>
 53                         <input type="submit" name="" id="" value="提交" /><br>
 54                         <input type="reset" value="重置"><br>
 55                     </form>
 56                 </div>
 57                 <!-- 修改的表单 -->
 58                 <div id="gai" style="display: none">
 59                     <form id="gaihouse" action="house/updatehouse.do" method="post"
 60                         style="width: 600px; padding: 20px">
 61                         id:<input type="text" name="id"><br>
 62                         关键字:<input type="text" name="keyword"><br>
 63                         地区:<input type="text" name="area"><br>
 64                         面积:<input type="text" name="squaremeter"><br>
 65                         租金:<input type="text" name="rent"><br>
 66                         租赁方式:<input type="text" name="renttype"><br>
 67                         房屋类型:<input type="text" name="housetype"><br>
 68                         <input type="submit" name="" id="" value="提交" /><br>
 69                         <input type="reset" value="重置"><br>
 70                     </form>
 71                 </div>
 72             </div>
 73 
 74         </div>
 75     </div>
 76     <!-- 对话框结束 -->
 77     <!-- 目录开始 -->
 78     <div data-options="region:'west',split:true" width=210>
 79         <div id="aa" class="easyui-accordion"
 80             style="width: 200px; height: 543px">
 81             
 82             <div title="用户管理" style="overflow: auto; padding: 10px" >
 83                 <ul>
 84                     <li class="lis"><a id="addhouse" class="easyui-linkbutton ab"
 85                         plain="true" >添加房屋信息(先用右边按钮)</a></li>
 86                     <li class="lis"><a href="<%=basePath %>/houselook.jsp" class="easyui-linkbutton ab"
 87                         plain="true">查看租房信息2</a></li>
 88                     <li class="lis"><a href="<%=basePath %>/house/selectAll2.do" class="easyui-linkbutton ab"
 89                         plain="true">查看租房信息3</a></li>
 90                     <li class="lis"><a href="houselook3.jsp" class="easyui-linkbutton ab"
 91                         plain="true">前往租房页面</a></li>
 92                     <li class="lis"><a href="#" class="easyui-linkbutton ab"
 93                         plain="true">修改用户</a></li>
 94                 </ul>
 95             </div>
 96         </div>
 97     </div>
 98     <!-- 底部声明 -->
 99     <div data-options="region:'south',split:true"
100         style="height: 40px; line-height: 40px; vertical-align: center; text-align: center;">
101         玛雅网络版权声明</div>
102     <!-- 目录结束 -->
103 </body>
104 </html>
105 <script type="text/javascript">
106 
107     $(function() {
108         $('#addhouse').form({    
109             url:'house/addhouse.do',
110             onSubmit: function(){
111                 return $('#addhouse').form('validate');//如果有为空则返回false阻止提交
112             },
113             success:function(data){    
114                 if(data=="true"){
115                     alert("添加成功");
116                 }else if(data=="false"){
117                     alert("请检查信息正确!");
118                 }
119             }    
120         });
121         
122         $('#table').datagrid({    
123             url : 'house/selectAll.do',
124             striped:true,//显示斑马线
125             autoRowHeight:false,//定义设置行的高度,根据该行的内容。设置为false可以提高负载性能。这里不设置,css中设置的行高无效
126             singleSelect:true,//只允许选择一行
127             pagination : true,
128             pageNumber : 1,
129             pageSize : 1,
130             pageList : [ 1, 3, 5 ],
131             
132             toolbar : [{
133                 iconCls : 'icon-edit',
134                 text : "添加",
135                 handler : function() {    
136                     var a = $(this).text();
137                     
138                     $('#zhong').dialog({
139                         width : 800,
140                         height : 500,
141                         title : a,
142                         //closed : false,
143                         cache : false,
144                         modal : true
145                     });
146                     
147                     
148                 }
149             },  '-',{
150                 iconCls : 'icon-edit',
151                 text : "修改",
152                 handler : function() {
153                     var a = $(this).text();
154                     $('#gai').dialog({
155                         width : 800,
156                         height : 500,
157                         title : a,
158                         //closed : false,
159                         cache : false,
160                         modal : true
161                     });
162                     $('#gai').dialog("open");
163                     var r = $("#table").datagrid("getSelected");//获取被选中的行,返回对象
164                     $("#gaihouse").form("load", r);//将被选中的信息放到弹出的的表单中,富文本编辑器的内容无法显示
165                 }
166             }, '-',
167             {
168                 iconCls : 'icon-cancel',
169                 text : "删除",
170                 handler : function() {
171                     var id=-1;
172                     id = $('#table').datagrid("getSelected").id;
173                     if(id>-1){
174                         var r1 = confirm("确定删除编号为  "+id+" 的房屋信息吗?");
175                         if(r1) {
176                             window.location.href="house/delhouse.do?id="+id;
177                             alert("删除成功");
178                         }
179                     }else{
180                         alert("请选中需要删除的商品");
181                     }
182                     
183                 }
184             } ],
185 
186              frozenColumns : [ [ {
187                  field : '',
188                 title : '',
189                 width : 20,
190                 checkbox : true
191             } ] ], 
192             columns : [ [ {
193                 field : "id",
194                 title : "信息编号",
195                 width:65
196             },{
197                 field : "keyword",
198                 title : "关键字",
199                 width:180
200             },  
201             {
202                 field : "area",
203                 title : "地区",
204                 width:60
205             }, {
206                 field : "squaremeter",
207                 title : "面积",
208                 width:60
209             }, {
210                 field : "rent",
211                 title : "租金",
212                 width:40
213             } , {
214                 field : "renttype",
215                 title : "租赁方式",
216                 width:60
217             } ,{
218                 field : "housetype",
219                 title : "房屋类型",
220                 width : 60
221             } ] ],
222             
223         }); 
224     });
225 </script>
复制代码
复制代码
 1  <%@ page language="java" contentType="text/html; charset=UTF-8"
 2     pageEncoding="UTF-8" import="java.util.List,com.hanqi.model.House,com.hanqi.controller.HouseController"%>
 3 <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
 4 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 5 <html>
 6 <head>
 7 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 8 <title>Insert title here</title>
 9 <script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
10 <script type="text/javascript"
11     src="jquery-easyui-1.5.1/jquery.easyui.min.js"></script>
12     <link rel="shortcut icon" href="img/logo1.jpg"/>
13 <link type="text/css" rel="stylesheet"
14     href="jquery-easyui-1.5.1/themes/icon.css"></link>
15 <link type="text/css" rel="stylesheet"
16     href="jquery-easyui-1.5.1/themes/default/easyui.css"></link>
17 <script type="text/javascript"
18     src="jquery-easyui-1.5.1/locale/easyui-lang-zh_CN.js"></script>
19 </head>
20 <body>
21 <form action="house/selectinfo.do" method="post">
22     区域:<input type="radio" name="area" id="s1" value="张店"/>
23     <label for="s1">张店</label>
24   <input type="radio" name="area" id="s2" value="淄川"/>
25     <label for="s2">淄川</label>
26   <input type="radio" name="area" id="s0" value="周村"/>
27     <label for="s0">周村</label><br>
28     租赁类型:<input type="checkbox" name="renttype" id="s3" value="整租"/>
29     <label for="s3">整租</label>
30   <input type="checkbox" name="renttype" id="s4" value="合租"/>
31     <label for="s4">合租</label>
32   <input type="checkbox" name="renttype" id="s5" value="其他"/>
33     <label for="s5">其他</label><br>
34     房屋类型:<input type="radio" name="housetype" id="s6" value="三室一厅"/>
35     <label for="s6">三室一厅</label>
36   <input type="radio" name="housetype" id="s7" value="自建房"/>
37     <label for="s7">自建房</label>
38   <input type="radio" name="housetype" id="s8" value="其他"/>
39     <label for="s8">其他</label><br>
40     关键字:<input type="text" name="keyword">
41     <input type="submit" value="查询">
42 </form>
43 
44 <%    
45     //HouseController hc=new HouseController();
46     List<House> list=(List<House>)request.getAttribute("list");
47     if(list!=null){
48         out.print("<table border='1'>");
49         for(House h:list){
50             out.print("<tr>");
51             out.print("<td>"+h.getKeyword()+"</td>");
52             out.print("<td>"+h.getArea()+"</td>");
53             out.print("<td>"+h.getSquaremeter()+"</td>");
54             out.print("<td>"+h.getRent()+"</td>");
55             out.print("<td>"+h.getRenttype()+"</td>");
56             out.print("<td>"+h.getHousetype()+"</td>");
57             out.print("</tr>");
58         }
59         out.print("</table>");
60     }
61 %>
62 </body>
63 </html>
复制代码

阿里巴巴 Java 开发规约插件初体验

mikel阅读(1594)

又一次来谈《阿里巴巴 Java 开发手册》,经过这大半年的版本迭代,这本阿里工程师们总结出来避免写出那么多 Bug 的规范,对于 Java 开发者简直就是必备开发利剑了。

针对这个手册,我之前也简单写过两篇简单的解读,手册的确是清晰的说明了日常开发中很容易踩的坑,我不要脸的把链接放一下:

这一次,阿里巴巴于 10 月 14 日在杭州云栖大会上,正式发布众所期待的《阿里巴巴 Java 开发规约》扫描插件!

简单了解一下这插件

该插件由阿里巴巴 P3C 项目组研发。P3C 是世界知名的反潜机,专门对付水下潜水艇,寓意是扫描出所有潜在的代码隐患。

代码已经开源,GitHub:https://github.com/alibaba/p3c
阿里介绍文章:https://mp.weixin.qq.com/s/IbibsXlWHlM59kfXJqRvZA#rd

据说插件支持了IDEA、Eclipse,在扫描代码后,将不符合规约的代码显示出来,甚至在 IDEA 上,我们还基于 Inspection 机制提供了实时检测功能,编写代码的同时也能快速发现问题所在,还实现了批量一键修复的功能。

那我就在 IDEA 下试一下吧。

IDEA 下如何使用?

1.打开 IDEA ,选择 File – Settings – Plugins – Browse repositories
打开 IDEA,选择 File – Settings – Plugins – Browse repositories 后,输入 alibaba 选中 Alibaba Java Coding Guidelines,点击 Install,截至目前已经快有了三千的安装量了。

alibaba guidelines

2.重启IDEA,开始使用
安装后,会提示你重启 IDEA 已启用插件,重启后就可以在你的项目上疯狂点击右键吧,当然菜单栏也会出现俩按钮,它有个快捷键是Ctrl+Shift+Alt+J,你也可以试试看。

checkStart

当然,选中某一个类,或者在这个类里边右键也可以

checkStartClass

3.扫描出坏代码
扫描代码后,将不符合规约的代码按 Blocker/Critical/Major 三个等级显示在下方,双击可以定位至代码处,右侧窗口还有针对代码的批量修复功能,简直不能更 666 了。

bad code

稍微解释下Blocker/Critical/Major三个等级,在 Snoar 中对代码规则有五个级别,这是前三个,翻译下就是:崩溃/严重/重要 ,也就是说前两级别是必须要处理掉的。

Eclipse 如何安装插件?

Eclipse 版插件支持4.2(Juno,JDK1.8+)及以上版本,我们提供自主的Update Site,通过 Help >> Install New Software 然后输入https://p3c.alibaba.com/plugin/eclipse/update
即可看到安装列表。大家可以通过 Help >> Check for Udates 进行插件新版检测

我没有在 Eclipse 上体验,你们有 Eclipse 的可以试试看。

总结一下

这个插件可以说对规范代码有重大意义,可以使我们少写一些 Bug,少抛一些异常,少踩一些坑,但真正要写出健壮代码最重要的还在于我们自己,这个插件还存在很多问题,一些容易产生 NPE 的级联调用、空对象什么的,好像它并不能检查出来,依赖工具永远是不可能解决所有问题的。

当然,它的中文版对我们是无比友好的。

一张图理清ASP.NET Core启动流程 - 『圣杰』 - 博客园

mikel阅读(1049)

来源: 一张图理清ASP.NET Core启动流程 – 『圣杰』 – 博客园

1. 引言

对于ASP.NET Core应用程序来说,我们要记住非常重要的一点是:其本质上是一个独立的控制台应用,它并不是必需在IIS内部托管且并不需要IIS来启动运行(而这正是ASP.NET Core跨平台的基石)。ASP.NET Core应用程序拥有一个内置的Self-Hosted(自托管)Web Server(Web服务器),用来处理外部请求。

不管是托管还是自托管,都离不开Host(宿主)。在ASP.NET Core应用中通过配置并启动一个Host来完成应用程序的启动和其生命周期的管理(如下图所示)。而Host的主要的职责就是Web Server的配置和Pilpeline(请求处理管道)的构建。

这张图描述了一个总体的启动流程,从上图中我们知道ASP.NET Core应用程序的启动主要包含三个步骤:

  1. CreateDefaultBuilder():创建IWebHostBuilder
  2. Build():IWebHostBuilder负责创建IWebHost
  3. Run():启动IWebHost

所以,ASP.NET Core应用的启动本质上是启动作为宿主的WebHost对象。
其主要涉及到两个关键对象IWebHostBuilderIWebHost,它们的内部实现是ASP.NET Core应用的核心所在。下面我们就结合源码并梳理调用堆栈来一探究竟!

2. 宿主构造器:IWebHostBuilder

在启动IWebHost宿主之前,我们需要完成对IWebHost的创建和配置。而这一项工作需要借助IWebHostBuilder对象来完成的,ASP.NET Core中提供了默认实现WebHostBuilder。而WebHostBuilder是由WebHost的同名工具类(Microsoft.AspNetCore命名空间下)中的CreateDefaultBuilder方法创建的。

从上图中我们可以看出CreateDefaultBuilder()方法主要干了六件大事:

  1. UseKestrel:使用Kestrel作为Web server。
  2. UseContentRoot:指定Web host使用的content root(内容根目录),比如Views。默认为当前应用程序根目录。
  3. ConfigureAppConfiguration:设置当前应用程序配置。主要是读取 appsettinggs.json 配置文件、开发环境中配置的UserSecrets、添加环境变量和命令行参数 。
  4. ConfigureLogging:读取配置文件中的Logging节点,配置日志系统。
  5. UseIISIntegration:使用IISIntegration 中间件。
  6. UseDefaultServiceProvider:设置默认的依赖注入容器。

创建完毕WebHostBuilder后,通过调用UseStartup()来指定启动类,来为后续服务的注册及中间件的注册提供入口。

3. 宿主:IWebHost

在ASP.Net Core中定义了IWebHost用来表示Web应用的宿主,并提供了一个默认实现WebHost。宿主的创建是通过调用IWebHostBuilderBuild()方法来完成的。那该方法主要做了哪些事情呢,我们来看下面这张【ASP.NET Core启动流程调用堆栈】中的黄色边框部分:

其核心主要在于WebHost的创建,又可以划分为三个部分:

  1. 构建依赖注入容器,初始通用服务的注册:BuildCommonService();
  2. 实例化WebHost:var host = new WebHost(…);
  3. 初始化WebHost,也就是构建由中间件组成的请求处理管道:host.Initialize();

3.1. 注册初始通用服务

BuildBuildCommonService方法主要做了两件事:

  1. 查找HostingStartupAttribute特性以应用其他程序集中的启动配置
  2. 注册通用服务
  3. 若配置了启动程序集,则发现并以IStartup类型注入到IOC容器中

3.2. 创建IWebHost

public IWebHost Build()
{
    //省略部分代码

    var host = new WebHost(
        applicationServices,
        hostingServiceProvider,
        _options,
        _config,
        hostingStartupErrors);
    }
    
    host.Initialize();

    return host;
}

3.3. 构建请求处理管道

请求管道的构建,主要是中间件之间的衔接处理。

而请求处理管道的构建,又包含三个主要部分:

  1. 注册Startup中绑定的服务;
  2. 配置IServer;
  3. 构建管道

请求管道的构建主要是借助于IApplicationBuilder,相关类图如下:

4. 启动WebHost

WebHost的启动主要分为两步:

  1. 再次确认请求管道正确创建
  2. 启动Server以监听请求
  3. 启动 HostedService

4.1. 确认请求管道的创建

从图中可以看出,第一步调用Initialize()方法主要是取保请求管道的正确创建。其内部主要是对BuildApplication()方法的调用,与我们上面所讲WebHost的构建环节具有相同的调用堆栈。而最终返回的正是由中间件衔接而成的RequestDelegate类型代表的请求管道。

4.2. 启动Server

我们先来看下类图:

从类图中我们可以看出IServer接口主要定义了一个只读的特性集合属性、一个启动和停止的方法声明。在创建宿主构造器IWebHostBuilder时我们通过调用UseKestrel()方法指定了使用KestrelServer作为默认的IServer实现。其方法申明中接收了一个IHttpApplication<TContext> application的参数,从命名来看,它代表一个Http应用程序,我们来看下具体的接口定义:

其主要定义了三个方法,第一个方法用来创建请求上下文;第二个方法用来处理请求;第三个方法用来释放上下文。而至于请求上下文,是用来携带请求和返回响应的核心参数,其贯穿与整个请求处理管道之中。ASP.NET Core中提供了默认的实现HostingApplication,其构造函数接收一个RequestDelegate _application(也就是链接中间件形成的处理管道)用来处理请求。

var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory);

4.3. 启动IHostedService

IHostedService接口用来定义后台任务,通过实现该接口并注册到Ioc容器中,它会随着ASP.NET Core 程序启动而启动,终止而终止。

5. 总结

结合源码,通过对ASP.NET Core运行调用堆栈的梳理,其启动流程的总体脉络一目了然,并且了解到主要的几个关键对象:

  1. 负责创建IWebHost的宿主构造器IWebHostBuilder
  2. 代表宿主的IWebHost接口
  3. 用于构建请求管道的IApplicationBuilder
  4. 中间件衔接而成的RequestDelegate
  5. 代表Web Server的IServer接口
  6. 贯穿请求处理管道的请求上下文HttpContext
  7. 可以用来注册后台服务的IHostedService接口

这一节就先从总体上对ASP.NET Core的运行原理有个基本的认识,后续我们再一一讲解这几个核心对象来加深理解。

谈谈数据监听observable的实现 - 萧秦 - 博客园

mikel阅读(948)

来源: 谈谈数据监听observable的实现 – 萧秦 – 博客园

一、概述

数据监听实现上就是当数据变化时会通知我们的监听器去更新所有的订阅处理,如:

var vm = new Observer({a:{b:{x:1,y:2}}});
vm.watch('a.b.x',function(newVal,oldVal){
    console.log(arguments);
});
vm.a.b.x = 11; //触发watcher执行 输出 11 1

数据监听是对观察者模式的实现,也是MVVM中的核心功能。这个功能我们在很多场景中都可以用到,可以大大的简化我们的代码。

二、现有MVVM框架中的Observable是怎么实现的

先看看各MVVM框架对Observable是怎么实现的,我们分析下它们的实现原理,常见的MVVM框架有以下几种:
1、knockout,老牌的MVVM实现

<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
var ViewModel = function(first, last) {
    this.firstName = ko.observable(first);
    this.lastName = ko.observable(last);
 
    this.fullName = ko.pureComputed(function() {
        return this.firstName() + " " + this.lastName();
    }, this);
};
 
ko.applyBindings(new ViewModel("Planet", "Earth")); 

早期微软是把每个属性转换成一个observable函数,通过函数对该属性进行取值赋值来实现的,缺点是改变了原属性,不能够像属性一样取值赋值。

2、avalon,国产框架特点是兼容IE6+

<div ms-controller="box">
    <div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h"  ms-click="click"></div>
    <p>{{ w }} x {{ h }}</p>
    <p>W: <input type="text" ms-duplex="w" data-duplex-event="change"/></p>
    <p>H: <input type="text" ms-duplex="h" /></p>
</div>
var vm = avalon.define({
 $id: "box",
  w: 100,
  h: 100,
  click: function() {
    vm.w = parseFloat(vm.w) + 10;
    vm.h = parseFloat(vm.h) + 10;
  }
});
avalon.scan()

avalon对数据监听堪称司徒的黑魔法,IE9+时利用ES5的defineProperty/defineProperties去实现,当IE不支持此方法时利用vbscript来实现。缺点是vbs定义后的对象不能够动态增删属性。

3、angular,大而全的mvvm解决方案

<div ng-app="myApp" ng-controller="myCtrl">
名: <input type="text" ng-model="firstName"><br>
姓: <input type="text" ng-model="lastName"><br>
<br>
姓名: {{firstName + " " + lastName}}
</div>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
    $scope.firstName = "John";
    $scope.lastName = "Doe";
});

ng对数据监听的实现,采用了AOP的编程思维,它对常用的dom事件xhr事件等进行封装,当这些事件被触发发,封装的方法中有去调用ng的digest流程,在此流程去检测数据变化并通知所有订阅,所以我们导致使用原生的setTimeout代替$timeout后需要自已去执行执行$digest()$apply(),缺点是需要对使用到的所有外部事件进行封装。

4、vue,现代小巧优雅(实际上是比avalon大一些)

<div id="demo">
  <p>{{message}}</p>
  <input v-model="message">
</div>
var demo = new Vue({
  el: '#demo',
  data: {
    message: 'Hello Vue.js!'
  }
})

vue对数据监听的实现就比较单一了,因为它只支持IE9+,利用Object.defineProperty一招搞定。缺点是不兼容低版本IE。

三、Observable的实现有哪些方法及思路

通过上面几个框架对比我们可以看出几种不同数据监听的实现方法,实际上还有很多的方式可以去实现的:
1、把属性转换为函数(knockout
2、IE9+使用defineProperty/definePropertiesvueavalon
3、低版本IE使用VBS(avalon
4、数据检测,对各事件进行封装,在封装的方法中调用digest(angular
5、利用__defineGetter__/__defineSetter__方法(avalon
6、把数据转换成dom对象利用IE8 dom对象的defineProperty方法或onpropertychange事件
7、利用Object.observe方法
8、利用ES6的Proxy对象
9、利用setInterval进行脏检测

那么我们就具体看下这些数据监听实现:
1、利用函数转换如ko.observable(),兼容所有

function observable(val){
    return function(newVal){
        if (arguments.length > 0){
            val = newVal;
            notifyChanges();
        }else{
            return val;
        }
    }
}
var data = {};
var data.a = observable(1);
var value = data.a() //取值
data.a(2); //赋值

2、利用defineProperty/defineProperties,兼容性IE9+

function defineReactive(obj, key, val){
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      return val;
    },
    set: function reactiveSetter(newVal) {
      val = newVal;
      notifyChanges();
    }
  });
}

3、利用__defineGetter__/__defineSetter__,兼容性一些mozilla内核的浏览器
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__

function defineReactive(obj, key, val){
  obj.__defineGetter__(key, function() {
    return val;
  });
  obj.__defineSetter__(key, function(newVal) {
    val = newVal;
    notifyChanges();
  });
}

4、利用vbs,兼容性低版本的IE浏览器,IE11 edge不再支持(avalon
先window.execScript得到parseVB的方法

Function parseVB(code)
    ExecuteGlobal(code)
End Function
window.execScript(parseVB_Code);

然后处理好数据属性properties生成get/set方法放在accessors,并把notifyChanges放到get/set中,然后动态生成以下vbs代码

Class DefinePropertyClass
    Private [__data__], [__proxy__]
    Public Default Function [__const__](d1, p1)
        Set [__data__] = d1: set [__proxy__] = p1
        Set [__const__] = Me
    End Function
    Public Property Let [bbb](val1)
        Call [__proxy__](Me,[__data__], "bbb", val1)
    End Property
    Public Property Set [bbb](val1)
        Call [__proxy__](Me,[__data__], "bbb", val1)
    End Property
    Public Property Get [bbb]
    On Error Resume Next
        Set[bbb] = [__proxy__](Me,[__data__],"bbb")
    If Err.Number <> 0 Then
        [bbb] = [__proxy__](Me,[__data__],"bbb")
    End If
    On Error Goto 0
    End Property
    Public Property Let [ccc](val1)
        Call [__proxy__](Me,[__data__], "ccc", val1)
    End Property
    Public Property Set [ccc](val1)
        Call [__proxy__](Me,[__data__], "ccc", val1)
    End Property
    Public Property Get [ccc]
    On Error Resume Next
        Set[ccc] = [__proxy__](Me,[__data__],"ccc")
    If Err.Number <> 0 Then
        [ccc] = [__proxy__](Me,[__data__],"ccc")
    End If
    On Error Goto 0
    End Property
    Public Property Let [$model](val1)
        Call [__proxy__](Me,[__data__], "$model", val1)
    End Property
    Public Property Set [$model](val1)
        Call [__proxy__](Me,[__data__], "$model", val1)
    End Property
    Public Property Get [$model]
    On Error Resume Next
        Set[$model] = [__proxy__](Me,[__data__],"$model")
    If Err.Number <> 0 Then
        [$model] = [__proxy__](Me,[__data__],"$model")
    End If
    On Error Goto 0
    End Property
    Public [$id]
    Public [$render]
    Public [$track]
    Public [$element]
    Public [$watch]
    Public [$fire]
    Public [$events]
    Public [$skipArray]
    Public [$accessors]
    Public [$hashcode]
    Public [$run]
    Public [$wait]
    Public [hasOwnProperty]
End Class

Function DefinePropertyClassFactory(a, b)
    Dim o
    Set o = (New DefinePropertyClass)(a, b)
    Set DefinePropertyClassFactory = o;
End Function

执行以上两段vbs代码得到observable对象

window.parseVB(DefinePropertyClass_code);
window.parseVB(DefinePropertyClassFactory_code);
var vm = window.DefinePropertyClassFactory(accessors, VBMediator);

function VBMediator(instance, accessors, name, value) {
    var accessor = accessors[name]
    if (arguments.length === 4) {
        accessor.set.call(instance, value)
    } else {
        return accessor.get.call(instance)
    }
}

5、在事件中触发检测digest,兼容所有(angular
以发XMLHttpRequest 为例

  var _XMLHttpRequest = window.XMLHttpRequest;
  window.XMLHttpRequest = function(flags) {
      var req;
      req = new _XMLHttpRequest(flags);
      monitorXHR(req); //处理req绑定触发数据检测及notifyChanges处理
      return req;
  };

6、把数据转换成dom节点再利用defineProperty方法或onpropertychange事件,这种极端的办法主要是用来处理IE8的,因为IE8支持defineProperty但只有DOM元素才支持

function data2dom(obj,key,val){
    if (!obj instanceof HTMLElement){
        obj = document.createElement('i');
    }
    //defineProperty or onpropertychange handle
    defineProperty(obj,key,val); //内部处理notifyChanges
    return obj;
}

这种方法的成本开销是很大的

7、利用Object.observe,在Chrome 36 beta版本中出现,但很多浏览器还没有支持已从ES7草案中移除

var data = {};
Object.observe(data, function(changes){
    changes.forEach(function(change) {
        console.log(change.type, change.name, change.oldValue);
    });
});

8、利用ES6的Proxy对象,未来的解决方案
https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/Proxy

//语法
var p = new Proxy(target, handler);

//示例
let setter = {
  set: function(obj, prop, value) {
    obj[prop] = value;
    notifyChanges();
  }
};

let person = new Proxy({}, setter);
person.age = 28; //触发notifyChanges

9、利用脏检测,兼容所有,主要用于没有很好办法的情况下
利用脏检测实现Object.defineProperty方法

function PropertyChecker(obj, key, val, desc) {
   this.key = key;
   this.val = val;
   this.get = function () {
     var val = desc.get();
     if (this.val == val) {
       val = obj[key];
       if (this.val != val) {
         desc.set(val);
       }
     }
     return val;
   };
   this.set = desc.set;
}
var checkList = [];
Object.defineProperty = function (obj, key, desc) {
  var val = obj[key] = desc.value != undefined ? desc.value : desc.get();
   if (desc.get && desc.set) {
     var property = new PropertyChecker(obj, key, val, desc);
     checkList.push(property);
   }
};

function loopIE8() {
 for (var i = 0; i < checkList.length; i++) {
    var item = checkList[i];
    var val = item.get();
    if (item.val != val) {
      item.val = val;
      item.set(val);
    }
  }
}
setTimeout(function () {
  setInterval(loopIE8, 200);
}, 1000);

四、监听数组变化

实际上以面说的这些仅仅是对数据对象进行监听,而数据中还包括数组,如:

var data = {a:[1,2,3]};
data.a.push(4);

这种操作也会使数据产生了变化,但是仅对getter setter进行定义是捕捉不到这些变化的。所以我们要单独针对数组做一些observable的处理。

基本思路就是重写数组的这些方法
1、push
2、pop,
3、shift
4、 unshift
5、splice
6、sort
7、reverse

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var arrayKeys = Object.keys(arrayMethods);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator() {
    var i = arguments.length;
    var args = new Array(i);
    while (i--) {
      args[i] = arguments[i];
    }
    var result = original.apply(this, args);
    var inserted;
    switch (method) {
      case 'push':
        inserted = args;
        break;
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    if (inserted) observe(inserted);
    notifyChanges(); //通知变化
    return result;
  });
});
function def(obj, key, val, enumerable) {
  obj = Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
function protoAugment(target, src) {
  target.__proto__ = src;
}

function copyAugment(target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i];
    def(target, key, src[key]);
  }
}

var _augmentArr = ('__proto__' in {})? protoAugment : copyAugment;
function augmentArr(arr){
  _augmentArr(arr, arrayMethods, arrayKeys);
};

使用时只需要调用augmentArr(arr)即可实现

五、数据监听存在哪些问题

目前主流的数据监听方案还是defineProperty + augmentArr的方式,已有不少的mvvm框架及一些observable类库,但是还存在一些问题:
1、所有的属性必须预先定义好

var data = new Observer({a:{b:1}});//这里没有定义a.c
data.$watch('a.c',function(newVal,oldVal){
    console.log(arguments);
});
data.a.c = 1; //此时,监听a.c的watcher是不生效的,因为没有提前定义c属性

2、属性被覆盖后监听失效

var data = new Observer({a:{b:1}});
data.$watch('a.b',function(newVal,oldVal){
    console.log(arguments);
});
data.a.b = 2; //生效
data.a = {b:3}; //此时b属性的原结构遭破坏,对b的监听失效

3、对数组元素的赋值是不会触发监听器更新的

var data = new Observer({a{c:[1,2,3]}});
data.$watch('a.c',function(newVal,oldVal){
    console.log(arguments);
});
data.a.c[1] = 22; //不会触发a.c的watcher

这个问题,不少框架中是提供了一个$set方法来赋值,这是个解决问题的办法,但是原生代码赋值仍是不生效的。

def(arrayProto, '$set', function $set(index, val) {
  if (index >= this.length) {
    this.length = Number(index) + 1;
  }
  return this.splice(index, 1, val)[0];
});

4、删除对象的属性也不会触发监听器更新

var data = new Observer({a:{b:1},c:'xyz'});
data.$watch('a',function(newVal,oldVal){
    console.log(arguments);
});
delete data.a; //不会触发a的watcher

同数组也可以父节点中定义一个$remove来实现

六、这些问题的解决方案

上述问题中:
1、第1、2其实是属于同一类的问题,就是因为这些notifyChanges直接在defineProperty时定义在属性中,当这个属性未定义或遭破坏时,那么对该属性的监听肯定是要失效的。对于这个问题的解决,我的思路是这样的

function Observer(data){
    this.data = data;
    var watches=[];
    //监听时,先把监听数据保存在该observer实例的watches中
    this.watch=function(path,subscriber,options){
        watches.push(new Watcher(path,subscriber,options));
    };
    //当publish时把watcher转换为subscriber绑定到对应的属性上
    this.publish = function(watch){
        var target = queryProperty(watch.path);
        var subscriber = new Subscriber(watch,target);
        target.ob.subscribes.add(subscriber );
    }   
}

每当重新赋新值时,会从根节点拉取watches重新publish,这样的话保证了赋新值时原来的监听数据不会被覆盖。

var ob = new Observer(data);
ob.watch('a.b',function(){
    console.log(arguments);
});

此watcher信息是保存在根节点的ob对象中,每一个object类型的属性都会对应一个ob对象,这样即使data.a = {b:123}重新赋值导致data.a.b的定义被覆盖,但是根节点并没有被覆盖,在它被得新赋值时我们可以重新调用父节点ob中的publish方法把watcher重新生效,这样的话这个问题就可以解决了。

2、第3个问题,其实很容易解决,比如vue中只需要修改一句代码就可以解决,也许是出于性能还其它的考虑它没有这么去做。即把数组的每个元素当做属性来定义

function observeArr(arr){
  for (var i = 0, l = arr.length; i < l; i++) {
    observeProperty(arr, i, arr[i]);
  }
}

3、第4个问题除了父节点中增加$remove方法我目前也没有想到什么好的办法,如果大家有什么好的想法可以跟我交流下。

七、我对数据监听的实现

既然研究了下这个领域的东西,也就顺便造了个轮子实现了一个数据observable的功能,用法大概如下:

var data = {a:{b:{x:1,y:2}},c:[1,2,3]};
var ob = new Observer(data);
data.$watch('a.b',function(){
    console.log(arguments);
},{deep:true})
data.a.b.x = 11;

主要是利用了es5的Object.defineProperty + augmentArr来实现的,代码400行左右。
https://github.com/liuhuisheng/actionjs/blob/master/src/observer.js

然后想支持下IE8写了个polifill,用脏检查实现了下
https://github.com/liuhuisheng/actionjs/blob/master/src/polifill.js

一直很懒终于总结了下做个笔记。

我的微型工作流引擎-功能解析及API设计 - 萧秦 - 博客园

mikel阅读(784)

来源: 我的微型工作流引擎-功能解析及API设计 – 萧秦 – 博客园

一、前言

上一篇我给大家介绍了我的工作流的模型和基本的设计,这篇我想详细说明下我这款工作流的功能及使用示例。这款工作流主要是面向开发者设计的,为了先让大家有个全局的认识,局部功能的设计实现就不细说了,后续有时间我会继续写文章向大家介绍。

二、功能详解及使用示例代码

1、配置流程引擎,一般在程序启动过程中调用(Global.asax.cs中)

//初始化流程引擎
BpmConfiguration
    .Instance()
    .Config(@"C:\Configration\BpmConfig.xml")
    .Start();

如果不指定配置文件,则默认从app.config或web.config中读取流程配置

//初始化流程引擎
BpmConfiguration
    .Instance()
    .Start();

当然还支持同时启动多个流程引擎,以提供SAAS程序的支持。

复制代码
//A租户引擎配置
BpmConfiguration
    .Instance("TenantA")
    .Config(@"C:\BpmConfigA.xml")
    .Start();

//B租户引擎配置
BpmConfiguration
    .Instance("TenantB")
    .Config(@"C:\BpmConfigB.xml")
    .Start();
复制代码

XML中的配置包括:数据库连接、日志配置文件、任务计划启动延时、任务计划执行周期、用户关系结构的映射、流程中变量类型的拓展等。

2、取得工作流上下文,即工作流的入口,所有的功能都集中在这个入口上。

var bpm = new BpmContext()
    .UseTransaction(true)
    .SetActor("萧秦");

当前对于不同的引擎实例,其上下文是不同的

var bpm = new BpmContext("TenantA");
var bpm = new BpmContext("TenantB");

不传构造参数时,返回的是默认实例。

3、事务支持,是否开启事务、提交、回滚。

bpm.UseTransaction(true);
bpm.Commit();
bpm.Rollback();

4、流程定义

复制代码
//新增流程定义
bpm.NewProcessDefinition("请假流程")
    .SetXmlFile(@"C:\Definition\demo1.xml")
    .SetCategory("分类1")
    .SetEffectDate(DateTime.Now)
    .SetExpireDate(DateTime.Now.AddDays(180))
    .SetMemo("memo1")
    .Create()   //创建流程定义
    .Parse()    //解析xml
复制代码

流程创建时,版本号是自动生成的,默认从1.0开始,当流程名称相同时,就会生成不同版本。
在xml中可定义不同的任务节点:开始节点、自动节点、人工节点、决策节点、发散节点、聚合节点、子流程节点、会签节点、等待节点、结束节点。
及连接任务节点的路由、人员分配情况、变量定义、事件动作等信息,可参照我上篇文章中的xml定义

//加载流程定义
var processDefinition = bpm.LoadProcessDefiniton("1");
processDefinition.Deploy();     //发布流程定义
processDefinition.Revoke();     //召回流程定义
processDefinition.Delete();     //删除流程定义

5、流程实例

复制代码
//发起流程实例
var process = bpm.NewProcessIntance("请假流程","萧秦");
process.SetVariable("project_id", 1399); //保存流程变量
process.Start();   //启动 
process.Suspend(); //挂起
process.Resume();  //恢复
process.Cancel();  //撤销
process.End();     //结束
复制代码

这里NewProcessInstance这个方法实例上有三个参数,第一个是流程定义ID,第二个是启动的业务ID,第三个是子流程的返回栈点ID,非子流程可以忽略。

//启动流程
var startTask = process.Start();
startTask.SetRecord("SO20150903001");       //保存表单数据(关联)
startTask.SetAttach("01", "02");            //保存附件信息(关联),可多个
startTask.SetVariable("var1", "value1");    //保存任务变量
startTask.Signal();                         //转交下一步

task.SetRecord用于保存当前表单数据id,数据本身保存在业务表中
task.SetAttach用于保存当前节点的附件id,附件信息则保存在附件管理表中
task.Signal流程流转的关键方法,根据流程定义触发token令牌离开动作

//审批任务
var task = bpm.LoadTaskInstance("00");
task.SetOpinion(SignResult.Passed, "我同意");  //设置审批意见
task.SetReceiver("颜经理");                    //设置下一步的审批人
task.Signal();                                //转交下一步

这里提供了SetReceiver的方法设置下一步的审批人,正常情况下流程定义中已经定义好了,是不需要再进行设置的,但是考虑在实际应用中可能会有把任务给指定领导审批的情况,在通达OA中也是可以设置下一步审批人,故添加了此方法,需要时可以调用,注意应用此方法会覆盖定义中对工作项owner属性的设置。

//任务委派
var task1 = bpm.LoadTaskInstance("01");
task1.AssignCandidate("小郑,小胡");              //添加任务候选人
task1.AssignCandidate("saler", ActorType.Role); //添加任务候选人
task1.AssignTo("李四");                         //把任务分配给李四

我的工作流中,对于工作任务只能有一个所有者(owner),一个实际操作者(actor),但可以拥有多个候选人(candidate)。
候选人是当前工作可分配的一范围限制或人员列表,owner是任务的拥有者,actor是owner考虑复杂委办关系后计算出的操作者。
task.AssignCandidate这个方法用于添加任务候选人,第二个参数是对象类型,可以直接添加一个角色、机构、用户组等。
task.AssignTo即分配任务,任务的分配状态包括以下几个状态

复制代码
public enum AssignStatus
{
    //未决
    Pending,

    //认领, 用户认领任务并接收任务输入数据
    Claim,

    //委办, 委派给另一个人(例如,经理)以代替他或她执行任务
    Depute,

    //到期, 没有在指定的时间段内处理批准任务
    Expire,

    //续订, 没有在给定的时间范围内处理此任务,则该任务将被续订,以便在另一时间段内执行
    Renew
}
复制代码

如果流程卡到某个节点很久,我们可以发催办消息
如果一个流程节点的确需要很久才能完成,我设计了一个当前工作进度汇报接口

//工作催办
task.Urge("很急,请经理尽快处理,在线等!");

//汇报当前工作进度
task.Report(0.6, "预计这个星期就能完成");

task.Urge会向任务实际拥有者发送一条催办通知,并生成催办历史。
task.Report会向任务的订阅者(所有关注当前流程任务的人)发送一条进度报告。

//查询变量
var var0 = process.GetVariable("project_id");
var var1 = task1.GetVariable("var1");
var var2 = task1.GetVariable<DateTime>("var2");
var var3 = task1.GetVariableObject("var3");

变量分为三种:
流程变量 会持久化,存在于整个的流程周期内
任务变量 会持久化,存在在当前的任务中
临时变量 不会持久化到数据库中,只存在于当前执行上下文中(executionContext)。
设置变量SetVariable 获取变量GetVariable
支持任意数据类型

6、中国特色审批方式,主要包括会签、加签(前加签、后加签、并加签)、减签、自由流
会签:一个任务由多个人参与共同做决策
加签:这个任务我自己觉得没有把握,想加入一个人或多个人跟我共同决策(在前加签顺序在当前决策者之前,后加签顺序在当前决策者之后,并加签不分顺序并行处理)
减签:跟加签相反,取消某人参与决策的资格
自由流:流程定义中没有,临时添加的动态路由直接把当前工作发送到指定的节点审批。
转会签:由单人决策的普通审批节点转成多人共同决策的会签节点,支持递归会签,即会签子节点可以继续转会签节点 。
转审批:由多人共同决策的会签节点转成单人决策的普通审批节点

在我在工作流中,会签设计了以下几个参数:
a 运行模式,并行时如发散节点,进入会签节点时会同时激活所有参与人的工作任务,串行时则有先后顺序,所以才有了前加签和后加签

复制代码
public enum RunMode
{
    //串行
    Serial,

    //并行
    Parallel
}
复制代码

b 决策模式,根据子节点的结果如何去决策会签节点

复制代码
public enum DecisionMode
{
    //主办人模式
    Sponsor, 
    
    //投票模式
    Vote, 
    
    //一票通过
    OnePass, 
    
    //一票否决
    OneVeto
}
复制代码

主办人模式:需要设置一个主办人,结果以主办人的决策为准,其它人的决策只是提供参考
投票模式:即设置一个通过的比例,由大家投票决定。支持设置每个人的投票权重。
一票通过:其实可以看作是投票模式通过率设置大于0%的一种。
一票否决:可以看作投票模式通过率设置100%的一种。
当然这里只是我把常用的几种模式列出来了,还可以自己拓展决策模式,只需要继承实现我定义的抽象类Decision即可。

c 是否等待,即还有人未表决,但目前已表决的情况已经可以确定会签结果的情况下,需不需要等待其它人表决完成后才继续转交下一步。

会签分两种,一种是流程定义中定义好的会签,一种是普通审批节点临时转成会签的。实际上中国式审批其实就是要灵活,如果在流程定义中定义好的,其实可以不用会签节点,用多个普通节点也可以去实现。会签节点的设计主要是为了转会签这个场景:就是当前普通审批节点的审批人觉得自己没有把握或者不想担责任,可以加入上级领导或其它更多的人一起来决策或提供参考意见。

示例:普通审批转会签 运行模式设置为并行,决策模式是权重投票,需要等待所有人都投票,通过线为65%

//转会签
var task2 = bpm.LoadTaskInstance("02"); 
task2.ToCountersign(RunMode.Parallel, DecisionMode.Vote, true, 0.65M);
task2.CountersignAdd(new Countersigner() { actor_id = "张三", vote_weight = 1 });
task2.CountersignAdd(new Countersigner() { actor_id = "李四", vote_weight = 0.5 });
task2.CountersignAdd(new Countersigner() { actor_id = "小五", vote_weight = 2 });

并行模式即为并加签,前加签、后加签的前提是串行模式,假设task2为串行、主办人模式、原审批人为萧秦

//前加签
task2.CountersignAddBefore("萧秦", new Countersigner() { actor_id = "张三"});

//后加签
task2.CountersignAddAfter("萧秦", new Countersigner() { actor_id = "李四", is_sponsor = true});

减签则相对比较简单了

//减签
task2.CountersignRemove("王五");

会签节点转普通审批,直接让一个人决策

//转审批
 task2.ToSinglesign("隔壁老王");

自由流模式,创建临时路直接跳转到指定节点进行审批

//自由流
var task3 = bpm.LoadTaskInstance("03");
task3.SetFreeRedirect("总经理审批");
task3.Signal();

7、回退机制
流程回退到指定节点

//流程实例指定任意节点回退
var process2 = bpm.LoadProcessInstance("02");
process2.Rollback("填写请假单");

任务实例回退到上一步

//当前工作任务回退到上一个审批节点
var task4 = bpm.LoadTaskInstance("04");
task4.Rollback();

8、工作委办
张三把某个任务直接委托给李四办理,支持递归委办关系,即张三委托给李四,李四再委托给王五,王五在委托给赵六…

bpm.NewDeputeService("张三", "李四")
   .ForTaskInstance("任务实例ID")
   .Depute();

把整个流程实例委托给李四,即此流程实例下所有的张三的任务都会委托给李四

bpm.NewDeputeService("张三", "李四")
   .ForProcessInstance("流程实例ID")
   .Depute();

把某个流程定义中的一个任务节点委托给李四,即所有的这个节点创建的所有任务实例如果是张三的任务都会委托给李四

bpm.NewDeputeService("张三", "李四")
   .ForTaskDefinition("任务定义ID")
   .Depute();

把某个流程定义委托给李四,即这个流程中创建的所有的任务实例,如果是张三的任务,在设置的生效期间中都会委托给李四

bpm.NewDeputeService("张三", "李四")
   .ForProcessDefinition("流程定义ID")
   .SetDateRange(DateTime.Now, DateTime.Now.AddDays(30)) //生效期间
   .SetMemo("这个月出差,这个流程都委托给李四代办")            //委托说明
   .Depute();

收回委托关系,只要将Revoke替换Depute动作即可

//收回委办工作
bpm.NewDeputeService("张三", "李四")
   .ForProcessInstance("流程实例ID")
   .Revoke();

9、关注订阅
这个功能跟委托相似,订阅后会收到流程动态或任务动态消息提醒,如:流程已创建、启动、挂起…,任务已创建、分配给谁、进度汇报、任务完成等等

复制代码
//关注订阅
bpm.NewSubscribeService("张三")
   .ForTaskInstance("任务实例ID")
   .Subscribe();

bpm.NewSubscribeService("张三")
   .ForProcessInstance("流程实例ID")
   .Subscribe();

bpm.NewSubscribeService("张三")
   .ForProcessDefinition("流程定义ID")
   .Subscribe();

bpm.NewSubscribeService("张三","李四","王五")
   .ForTaskDefinition("任务定义ID")
   .Subscribe();
复制代码

取消订阅,一样只需要把Subscribe改为Unsubscribe即可

//取消订阅
bpm.NewSubscribeService("李四")
   .ForProcessDefinition("采购流程")
   .Unsubscribe();

10、数据查询
查询我没有提供接口,直接开放数据库查询比我提供的接口会更加灵活,比如:
a 查询已发布的流程定义

select * from bpm_definition_process where state = 'Deploy'

流程定义状态包括

复制代码
public enum ProcessDefinitionState
{
    //创建
    Create,

    //解析
    Parse,

    //发布
    Deploy,

    //回收
    Revoke,

    //删除
    Delete
}
复制代码

b 我的流程

select * from bpm_instance_process where state = 'Run' and starter = '萧秦'

流程状态包括

复制代码
public enum ProcessState
{
    //创建
    Create,

    //运行
    Run,

    //挂起
    Pending,

    //终止
    Termination,

    //完成
    Complete,

    //取消
    Cancel
}
复制代码

c 我的待办任务

select * from bpm_instance_task where state = 'Run' and actor_id = '萧秦'

待办任务包括了别人委托给你的任务,如果只想看属于自己的任务则可以

select * from bpm_instance_task where state = 'Run' and owner_id = '萧秦'

任务状态包括

复制代码
public enum TaskState
{
    //创建, 任务已被创建
    Create,

    //阻塞, 到达线中有阻塞任务还未完成
    Blocking,

    //启动
    Run,
   
    //完成, 用户已经完成任务并提供了任务的输出数据
    Complete,

    //失败, 用户已经完成任务, 并且提供了错误消息
    Failure,

    //回退
    Rollback
}
复制代码

d 查询任务的候选人信息

select * from bpm_instance_assignment where task_instance_id = 'ID'

e 查询我的消息

select * from bpm_application_notify where state='Unread' and reciever_id = '萧秦'

其它查询就不再举例了

三、总结

之前我有说过我开发这个的引擎的目的是为了在做项目时,有一个体积轻巧,引入方便的单dll文件(发布后大小为1.1M)的工作流引擎,接口也简单易于二次开发,支持多数据库并且功能还算强大。从前年开始的简易版本设计开发到现在基本成形,测试也是花费了大量的时间,可能还有问题没有测到,不过现在基本稳定。接下来如果有时间我会慢慢跟大家介绍功能细节的设计和实现,还有什么功能我考虑不周全的或意见或有哪部分想详细了解的都可以给我留言。联系13606021792

我的微型工作流引擎设计 - 萧秦 - 博客园

mikel阅读(953)

来源: 我的微型工作流引擎设计 – 萧秦 – 博客园

一、前言

提到工作流很多人就会想到OA,的确OA就是典型的工作流的应用,但是工作流并不仅仅局限于OA,工作流应该算是基础框架软件,主要用于流程的重组和优化,它有广阔的应用领域。在java下有很多优秀的开源工作流可以选择比如activit5、jpbm4等,在.net下却几乎找不到令人满意的工作流引擎可用。当然不是说.net下没有开源的只是有些国产开源的但看了代码后就一点兴趣都没有了,且不说代码质量如何,还引入了一大堆的东西,想在项目中应用也是非常困难。鉴于此我还是决定自己开发一款.NET微型工作流引擎。

二、基本说明

为什么叫微型工作流引擎?就是超轻量级,以方便在项目中轻便的使用,比如只有一个类库dll,大小也就几百k到1M左右,不过我们要先回过头来看看工作流系统,它实在是太大了,它应该包括:
1、工作流引擎
2、工作流设计器
3、工作流管理系统
4、表单设计器

目前来说的我只实现了核心引擎,流程定义也只能先在xml中编辑然后读取到引擎中或者直接定义到数据库中,但整个流程是能够正常流转。至于流程设计器、表单设计器、工作流管理系统这个我有精力了再慢慢开发。这里我完成的只是很小的一块,但是是工作流的核心,可以很方便的嵌入到业务系统中应用。

引擎主要提供了对于工作流定义的解析以及流程流转的支持。工作流定义文件描述了业务的交互逻辑,工作流引擎通过解析工作流定义文件按照业务的交互逻辑进行业务的流转,工作流引擎通常通过参考某种模型来进行设计,通过调度算法来进行流程的流转(流程的启动、终止、挂起、恢复等),通过各种环节调度算法来实现对于环节的流转(环节的合并、分叉、选择、条件性的选择等)。

三、初步印象

1、从概念开始解释估计大家都会看不下去了。我们先拿一个简单实例来看看,新建一个项目,引用我的工作流引擎类库(Chitu.Bpm.dll,取名为赤兔)。
在项目启动时配置流程引擎(Global.asax.cs中),如下:

//初始化流程引擎
BpmConfiguration
    .Instance()
    .Config(@"C:\Configration\BpmConfig.xml")
    .Start();

在项目中使用时,比如新建流程定义:

复制代码
//取得工作流上下文
var bpm = new BpmContext()
    .UseTransaction(true)
    .SetActor("萧秦");

//新增流程定义
bpm.NewProcessDefinition("请假流程")
    .SetXmlFile(@"C:\Definition\demo1.xml")
    .SetCategory("分类1")
    .SetEffectDate(DateTime.Now)
    .SetExpireDate(DateTime.Now.AddDays(180))
    .SetMemo("memo1")
    .Create()  //创建流程定义,只生成bpm_definition_process表
    .Parse()   //解析xml
    .Deploy(); //发布流程
复制代码

启动流程:

//启动流程
var process = bpm.NewProcessIntance("请假流程ID", "萧秦(业务ID)");   //创建流程实例
process.SetVariable("流程变量1", "值1");                     //设置流程变量
process.Start();

人工任务节点转交下一步:

//任务完成
var task = bpm.LoadTaskInstance("任务ID");
task.SetVariable("任务变量2", "xx");
task.Signal(); //触发令牌流转

所有的操作都通过Facade模式集中到BpmContext中,操作简单方便。

2、接下来我们先看看流程定义的XML,以下是我捏造的一个流程,以便把各种节点都放进去了。

复制代码
<?xml version="1.0" encoding="UTF-8"?>

<process name="样板房装修流程">
  
  <start name="装修申请">
    <transition to="装修方案设计" >
      <action class="Namespace.MyActionHandler"></action>
    </transition>
  </start>

  <task name="装修方案设计">
    <transition to="装修方案审核">
      <action script="log.Debug('装修方案审核 action test');"></action>
    </transition>
  </task>

  <decision name="装修方案审核">
    <transitions>
      <transition to="装修筹备"     condition-expression="variable.pass == true"></transition>
      <transition to="装修方案设计" condition-expression="variable.pass != true"></transition>
    </transitions>

    <events>
      <action event="enter" class="enterHandlerClass"></action>
      <action event="leave" class="leaveHandlerClass"></action>
    </events>

    <assignments>
      <assignment owner="{process.starter}"></assignment>
    </assignments>

    <variables>
      <variable type="boolean" name="IsPass" access="read,required"></variable>
    </variables>
  </decision>
 
  <fork name="装修筹备">
    <transition to="装修合同签定"></transition>
    <transition to="等待装修工人到位"></transition>
    <transition to="装修材料预算"></transition>
  </fork>

  <sign name="装修合同签定"  necessary="false" async="true">
    <transition to="装修施工"></transition>
  </sign>

  <wait name="等待装修工人到位">
    <transition to="装修施工"></transition>
  </wait>

  <task name="装修材料预算">
    <transition to="材料采购子流程"></transition>
  </task>

  <subflow name="材料采购子流程">
    <transition to="装修施工"></transition>
  </subflow>

  <join name="装修施工">
    <transition to="施工验收/付款"></transition>
  </join>

  <auto name="施工验收/付款">
    <transition to="装修完成"></transition>
  </auto>

  <end name="装修完成">  
  </end>

  <events>
    <action event="process-start" class="TestStartHandler"></action>
    <action event="process-end" class="TestEndHandler"></action>
  </events>

  <variables>
    <variable type="string" name="start_id" access="readonly,required"></variable>
    <variable type="string" name="start_person"></variable>
  </variables>
</process>
复制代码

定义的根节点为流程(process),流程下为各个任务节点(node),任务节点分为:
start       开始节点
auto       自动节点
task       人工节点
decisioin 决策节点
fork        发散节点
join        聚合节点
sublfow  子流程节点
sign       会签节点
wait       等待节点
end        结束节点

任务节点下可以包括路由(transition)动作(action)及人员分配(assignment)变量定义(variable)
其中action包括几种类型:1、class 2、script 3、SQL 4、webservice 5、expression
在script或expression中可以直接访问process.xxx属性或task.xxx属性或variable.xxx简化了动态C#语句的使用。
当然XML定义中还有很多其它的属性定义我这里也没有都列出来,以后用到了再仔细说。

3、关于数据库设计,这里仅仅是流程流转核心所需要的表,表之间都没有拉关系
image

  四、部分功能剖析

a、我把它划分为主要的几大模块: 引擎配置、流程定义、实例流转、日志处理、计划任务
引擎配置:配置引擎实例的数据库连接、日志配置、参数设定等。
流程定义:利用xml来描述流程,主要定义任务节点,路由、动作事件、变量、人员分配等
实例流转:根据定义运行流程实例
日志处理:输出日志
任务计划:会启动一个服务,用于处理比如延时启动,任务过期等

b、流转中的关键性类设计包括:
1、流程对象(Process)
2、工作任务(Task)
3、路由(Trasition)
4、令牌(Token)
5、事件总线与动作处理 (EventBus、ActionHandler)
6、人员分配及委托机制(Assignment、Depute)
7、流程回退处理(RollbackService)
8、消息服务(NotifyService)

c、通常引擎控制流程调度流转核心的调度算法主要有FSM以及PetriNet两种,基于调度算法来完成流程的流转:
1、FSM(有限状态机)
FSM 的定义为包含一组状态集(states)、一个起始状态(start state)、一组输入符号集(alphabet)、一个映射输入符号和当前状态到下一状态的转换函数(transition function)的计算模型。当输入符号串,模型随即进入起始状态。它要改变到新的状态,依赖于转换函数。在有限状态机中,会有有许多变量,例如,状态机有很多与动作(actions)转换(Mealy机)或状态(摩尔机)关联的动作,多重起始状态,基于没有输入符号的转换,或者指定符号和状态(非定有限状态机)的多个转换,指派给接收状态(识别者)的一个或多个状态,等等。遵循FSM流程引擎通过状态的切换来完成流程的流转。
2、PetriNet
信息流的一个抽象的、形式的模型。指出一系统的静态和动态性质。PetriNet通常表示成图。遵循PetriNet流程引擎通过令牌来决定流程的流转。
我采用的是第二种PetriNet算法。用Token来表示当前实例运行的位置,也利用token在流程各个点之间的转移来表示流程的推进,如下图所示:
image001
令牌流转逻辑,我把以下类方法都做一个简化省略了路由选择及节点处理细节,好让大家明白令牌的流转:

复制代码
//令牌Token类中Signal
public void Signal() 
{
    fromTask.Leave(executeContext);
}

//任务Task类中的Leave
public void Leave(ExecutionContext executionContext)
{
    transition.Take(executionContext);
}

//路由Transition类中的Take
public void Take(ExecutionContext executionContext)
{
    toTask.Enter(executionContext);
}

//任务Task类中的Enter
public void Enter(ExecutionContext executionContext)
{
    Run(executionContext);
}
复制代码

至此令牌成功的从一个节点转移到下一个节点了,令牌的流转是工作流的关键,当然不同的节点处理是有所不同的,其中最复杂的当数发散节点及聚合节点了。这里就介绍到这里,不再给大家详细介绍了。

d、目前我引擎中实现的主要包括以下功能:
1、解释过程定义
2、控制过程实例—创建、激活、挂起、终止等
3、控制流程调度流转
4、自定义动作及事件发布
5、流程变量及工作变量处理
6、任务计划,比如延时启动,任务过期等
7、委托服务,委托代办
8、回退服务,回退到任意节点或召回
9、消息服务,比如认领通知、待办提醒、催办消息…
10、流程任务监控服务
11、日志处理及历史记录
12、任务分配与认领
13、参与者组织模型接口

五、总结

目前我的这款工作流引擎还在继续完善当中,我总结下它的优缺点:
优点:
1、它是一款超轻量极或者说是微型的工作流引擎,而且绿色无污染,它只有一个dll,大小仅1M左右。
2、它目前支持SQL Server、MySql、Oracle、SQLite、PostgreSql等多种数据库
3、体型上来说它虽然是微型,但功能上并不算微型,它的设计结合了现代的OA及传统工作流、基本上可以实现我们大多数的功能需要。
4、它其实是面向开发者设计的,从上面初始印象中的实例代码中大家可以看到,它的接口是很集中、精简、友好的,让开发者容易理解而且使用起来更方便简单。所以它更适合嵌入到项目中开发。
缺点:
1、它现在没有流程设计器、管理系统、表单设计器等,充其量只能算是一个类库,并不是直接拿来就可以使用。
2、目前刚刚完成第一个内部版本,而且目前只在我们内部项目中使用,所以它不够成熟,虽然我们会持续的改进和完善。
3、缺乏成功应用的案例。
对于我们自己来说,这些缺点都是我们需要继续努力的地方,可能还需要大量的时间来完成。目前来说我们还不打算开源,等它慢慢稳定成熟后我们会考虑是不是开源出来。如果大家有好的建议或有哪方面的疑惑我很乐意给大家解答,或者你也在设计开发自己的工作流,我们可以相互交流下。联系13606021792

快速开发之代码生成器(asp.net mvc4 + easyui + knockoutjs) - 萧秦 - 博客园

mikel阅读(925)

来源: 快速开发之代码生成器(asp.net mvc4 + easyui + knockoutjs) – 萧秦 – 博客园

一、前言

作为一个码农这么多年,一直在想怎么提高我们的编码效率,关于如何提高编码效率,我自己的几点体会

1、清晰的项目结构,要编写代码的地方集中
2、实现相同功能的代码量少并且清晰易懂
3、重复或有规律的代码应该自动生成

在这里我就讨论下代码生成的问题。

二、关于代码生成器

刚毕业时我也非常迷信代码生成器,喜欢在网上找一些代码生成器及相关的源码,喜欢在和网友讨论哪款生成器最好用,但实际上很少真正用这些东西来开发项目,原因很简单:
1、生成出来的代码不是我们要的代码
2、生成后的代码再修修改改,其实还没有我的ctrl+c和ctrl+v速度快。
3、生成的基本上是实体类及SQL拼接代码,现在直接用linq或一些好用的orm多方便,谁还用SQLHelper加SQL文拼接?
4、b/s项目中没有一个生成器能很好的能生成UI层代码及前端交互的js代码,即使能生成也是简单的页面。

所以,我劝大家不要迷信代码生成器了。它的确可以提高我们的效率,但是并不是网上你找一个生成器就行的。代码生成器它只是一个模板引擎而已,最重要的不是代码生成器本身,而是对一类功能或一类页面的代码规范,对自己代码的提炼,提炼出一个通用的模板。

比如我们常见的查询页面,录入页面等,我们只要提炼一个标准的查询页面的代码,包括前台html,前台js,后台控制器,后台数据服务。然后把它写成模板,再利用模板引擎就可以生成我们需要的代码了。

代码生成器本身就是模板引擎,所以我觉得最好的代码生成器不是网上流传的那些可以生成三层架构代码的软件,而是微软的razor引擎,非常简洁易懂,而且做过ASP.NET mvc项目的朋友应该也很熟悉。我个人觉得这是用来做代码生成最好的引擎。

三、页面模板

我们还是会想要快速开发,比如我选择了一些设定之后,就可以直接生成我想要的代码包括html及js,拷贝到项目中就可以直接运行,运行后就看到我想要的页面,基本功能都有。当然这里所说的快速开发是建立在我对页面功能的提炼模板之上的。实际上我提炼了三种模板:
1、查询页面
这个模板可以解决大部分的查询报表业务功能
image

2、编辑页面
这个编辑模板可以解决基本上所有的录入功能,因为包括了主表,及多个从表(1:N或1:1)录入,而且可以一次性在同一事务中保存。并且定义了很多触发前后事件用于写一些业务处理,并且做到差异更新。
image

3、查询编辑页面,可以查询也可以直接在grid中编辑,这个页面用于做一些简单单据或一些基础数据页面。image

四、代码生成原理

把以上页面代码做成razor模板,razor模板 + 设定选项 ==razor引擎==> 页面代码

怎么利用razor引擎,其实有以下几种方法:
1、直接利用mvc的view输出功能,以下为关键代码

var stringWriter = new StringWriter();
var viewContext = new ViewContext(controllerContext, view, viewData, TempData, stringWriter);
view.Render(viewContext, stringWriter);
var result = stringWriter.ToString();

用这种方法的优点在于不需要引入第三方类库,直接调用mvc视图的Render方法生成,而且效率很高,缺点是controllerContext及view对象的构建获取非常复杂。这种方法适用于有洁辟的码农们,我属于这一种。

2、利用第三方类库RazorEngine输出,以下为关键代码

var template = "Hello @Model.Name! Welcome to Razor!";
var viewData = new { Name = "World" });
var result = Razor.Parse(template, viewData);

这代码很清爽,一目了然,只不过要引入RazorEngine类库,而且效率不如前者。

五、代码生成页面的源码

我们模板准备好了,引擎准备好了,那么还需要一个数据输入viewData,我们做用户界面的目的也就是为了更好的定义这个viewData。
UI展现主要是用了easyui 及JQuery插件smartwizard
前端交互主要是采用了knockoutjs
table表格的行拖拉是采用JQuery插件tableDnD
后台用webapi来处理请求,代码有点长:

Index.cshtml

复制代码
@{
    ViewBag.Title = "代码生成";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section head{
    <link href="~/Content/js/jquery-plugins/smartwizard/smart_wizard.css" rel="stylesheet" />
    <style type="text/css">
        div#navigation{float: left;width: 180px;}
        div#wrapper{float: right;width: 100%;margin-left: -185px;}
        div#wizard{margin-left: 185px;}
        ul.anchor{margin:0 0 10px 0 !important;}
        ul li{margin-left:-16px;}
        .grid .z-txt{margin:0 -3px;width:90%;} 
        .grid input{width:90%;}
        .grid input[type=checkbox]{cursor:default;}
        .grid select{width:80%;padding:0 !important;height:22px;}
        .grid select + a{margin:5px;}
        .tDnD_whileDrag{background-color: #FBEC88 !important;}
    </style>
}

@section scripts{
    <script src="~/Content/js/jquery-plugins/smartwizard/jquery.smartWizard.js"></script>
    <script src="~/Content/js/jquery-extend/jquery.tablednd.js"></script>
    @Scripts.Render("~/Resource/Sys/Generator.js")
    <script type="text/javascript">
        $(function () {
            ko.applyBindings(new viewModel());
        });
    </script>
}

<div id="container">
    <div id="navigation">
        <div class="panel-header" style="width: 168px; border-width: 0; background: #FAFAFA;">
            代码类别 
            <input type="text" class="z-txt" data-bind="easyuiCombobox:codetype" />
            <div style="margin:1px;"></div>
            数据库名 
            <input type="text" class="z-txt" data-bind="easyuiCombobox:database" />

            <div style="margin:5px;"></div>
             <div  data-bind="autoheight:60"  style="width: 172px; border-width: 0;margin:0;padding:0; background: #FAFAFA; overflow:auto;">
                <ul data-bind="easyuiTree:tabletree"></ul>
            </div>
        </div>
    </div>
    <div id="wrapper">
        <div id="wizard" class="swMain" style="width:100%"></div>
    </div>
</div>

<script id="template-searchEdit" type="text/html">
    <ul>
        <li><a href="#step-1">
            <label class="stepNumber">1</label>
            <span class="stepDesc">设置条件部<br />
                <small>定义查询条件</small>
            </span>
        </a></li>
        <li><a href="#step-2">
            <label class="stepNumber">2</label>
            <span class="stepDesc">设置数据列<br />
                <small>定义查询显示的数据字段</small>
            </span>
        </a></li>
        <li><a href="#step-3">
            <label class="stepNumber">3</label>
            <span class="stepDesc">其它设置<br />
                <small>修改其它代码生成设置</small>
            </span>
        </a></li>
    </ul>

    <div id="step-1" class="step">
        <h2 class="StepTitle">第一步 请勾选要查询的字段</h2>
        <div>  
            <div style="width:200px;float:left;overflow:auto;" data-bind="autoheight:172">
                    <ul data-bind="easyuiTree:searchEdit.columntree"></ul>
            </div>  
            <div style="float:left;overflow:auto" data-bind="autoheight:172,autowidth:405">
                <table class="grid">
                    <thead>
                        <tr>
                            <th style="width:50px">字段</th>
                            <th style="width:120px">显示名称</th>
                            <th style="width:120px">控件类型</th>
                            @*<th >参数</th>*@
                            <th style="width:80px">查询逻辑</th>
                        </tr>
                    </thead>
                    <tbody data-bind="foreach:form.conditions">
                        <tr data-bind="attr:{id:$index}">
                            <td data-bind="text:field" style="text-align:left"></td>
                            <td><input type="text" class="z-txt" data-bind="value:title"/></td>
                            <td><select class="z-txt"  data-bind="options:$root.data.input,value:type"></select></td>
                            @*<td><input type="text" class="z-txt" data-bind="value:options"/></td>*@
                            <td><select class="z-txt"  data-bind="options:$root.data.compare,value:cp"></select></td>
                        </tr>

                    </tbody>
                </table>
            </div>  
        </div>
    </div>
    <div id="step-2" class="step">
        <h2 class="StepTitle">第二步 请勾选要显示的数据字段</h2>
        <div style="width:200px;float:left;overflow:auto;" data-bind="autoheight:172">
            <ul data-bind="easyuiTree:searchEdit.columntree2"></ul>
        </div>  
        <div style="float:left;overflow:auto" data-bind="autoheight:172,autowidth:405">
            <table class="grid">
                <thead>
                    <tr>
                        <th style="width:50px">字段</th>
                        <th style="width:100px">题头</th>
                        <th style="width:30px">隐藏</th>
                        <th style="width:30px">排序</th>
                        <th style="width:50px">对齐</th>
                        <th style="width:40px">宽度</th>
                        <th style="width:50px">格式化</th>
                        <th style="width:50px">编辑器</th>
                    </tr>
                </thead>
                <tbody data-bind="foreach:form.columns">
                    <tr data-bind="attr:{id:$index}">
                        <td data-bind="text:field" style="text-align:left"></td>
                        <td><input type="text" class="z-txt" data-bind="value:title" /></td>
                        <td><input type="checkbox" data-bind="checked:hidden"/></td>
                        <td><input type="checkbox" data-bind="checked:sortable"/></td>
                        <td><select class="z-txt"  data-bind="options:$root.data.align,value:align" ></select></td>
                        <td><input type="text" class="z-txt" data-bind="value:width" /></td>
                        <td><select class="z-txt"  data-bind="options:$root.data.formatter,optionsText:'text',optionsValue:'value',value:formatter" ></select></td>
                        <td><select class="z-txt"  data-bind="options:$root.data.editor,optionsText:'text',optionsValue:'value',value:editor" ></select></td>
                    </tr>
                </tbody>
            </table>
        </div>  
    </div>

     <div id="step-3" class="step">
        <h2 class="StepTitle">第三步 其它设置</h2>

        <div class="container_12">
            <div class="grid_1 lbl">业务区域</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.area"/></div>

            <div class="clear"></div>

            <div class="grid_1 lbl">控制器名称</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.controller"/></div>

            <div class="clear"></div>

            <div class="grid_1 lbl">生成路径</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.path"/></div>
        </div>
    </div>
</script>

<script id="template-search" type="text/html">
    <ul>
        <li><a href="#step-1">
            <label class="stepNumber">1</label>
            <span class="stepDesc">设置条件部<br />
                <small>定义查询条件</small>
            </span>
        </a></li>
        <li><a href="#step-2">
            <label class="stepNumber">2</label>
            <span class="stepDesc">设置数据列<br />
                <small>定义查询显示的数据字段</small>
            </span>
        </a></li>
        <li><a href="#step-3">
            <label class="stepNumber">3</label>
            <span class="stepDesc">其它设置<br />
                <small>修改其它代码生成设置</small>
            </span>
        </a></li>
    </ul>

    <div id="step-1" class="step">
        <h2 class="StepTitle">第一步 请勾选要查询的字段</h2>
        <div>  
            <div style="width:200px;float:left;overflow:auto;" data-bind="autoheight:172">
                    <ul data-bind="easyuiTree:searchEdit.columntree"></ul>
            </div>  
            <div style="float:left;overflow:auto" data-bind="autoheight:172,autowidth:405">
                <table class="grid">
                    <thead>
                        <tr>
                            <th style="width:50px">字段</th>
                            <th style="width:120px">显示名称</th>
                            <th style="width:120px">控件类型</th>
                            @*<th >参数</th>*@
                            <th style="width:80px">查询逻辑</th>
                        </tr>
                    </thead>
                    <tbody data-bind="foreach:form.conditions">
                        <tr data-bind="attr:{id:$index}">
                            <td data-bind="text:field" style="text-align:left"></td>
                            <td><input type="text" class="z-txt" data-bind="value:title"/></td>
                            <td><select class="z-txt"  data-bind="options:$root.data.input,value:type"></select></td>
                            @*<td><input type="text" class="z-txt" data-bind="value:options"/></td>*@
                            <td><select class="z-txt"  data-bind="options:$root.data.compare,value:cp"></select></td>
                        </tr>

                    </tbody>
                </table>
            </div>  
        </div>
    </div>
    <div id="step-2" class="step">
        <h2 class="StepTitle">第二步 请勾选要显示的数据字段</h2>
        <div style="width:200px;float:left;overflow:auto;" data-bind="autoheight:172">
            <ul data-bind="easyuiTree:searchEdit.columntree2"></ul>
        </div>  
        <div style="float:left;overflow:auto" data-bind="autoheight:172,autowidth:405">
            <table class="grid">
                <thead>
                    <tr>
                        <th style="width:50px">字段</th>
                        <th style="width:100px">题头</th>
                        <th style="width:30px">隐藏</th>
                        <th style="width:30px">排序</th>
                        <th style="width:50px">对齐</th>
                        <th style="width:40px">宽度</th>
                        <th style="width:50px">格式化</th>
                    </tr>
                </thead>
                <tbody data-bind="foreach:form.columns">
                    <tr data-bind="attr:{id:$index}">
                        <td data-bind="text:field" style="text-align:left"></td>
                        <td><input type="text" class="z-txt" data-bind="value:title" /></td>
                        <td><input type="checkbox" data-bind="checked:hidden"/></td>
                        <td><input type="checkbox" data-bind="checked:sortable"/></td>
                        <td><select class="z-txt"  data-bind="options:$root.data.align,value:align" ></select></td>
                        <td><input type="text" class="z-txt" data-bind="value:width" /></td>
                        <td><select class="z-txt"  data-bind="options:$root.data.formatter,optionsText:'text',optionsValue:'value',value:formatter" ></select></td>
                    </tr>
                </tbody>
            </table>
        </div>  
    </div>

     <div id="step-3" class="step">
        <h2 class="StepTitle">第三步 其它设置</h2>

        <div class="container_12">
            <div class="grid_1 lbl">业务区域</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.area"/></div>

            <div class="clear"></div>

            <div class="grid_1 lbl">控制器名称</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.controller"/></div>

            <div class="clear"></div>

            <div class="grid_1 lbl">生成路径</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.path"/></div>
        </div>
    </div>
</script>

<script id="template-edit" type="text/html">
    <ul>
        <li><a href="#step-1">
            <label class="stepNumber">1</label>
            <span class="stepDesc">设置主表编辑区<br />
                <small>定义主表编辑字段</small>
            </span>
        </a></li>
        <li><a href="#step-2">
            <label class="stepNumber">2</label>
            <span class="stepDesc">设置明细数据页签<br />
                <small>定义明细表及页签</small>
            </span>
        </a></li>
        <li><a href="#step-3">
            <label class="stepNumber">3</label>
            <span class="stepDesc">其它设置<br />
                <small>修改其它代码生成设置</small>
            </span>
        </a></li>
    </ul>

    <div id="step-1" class="step">
        <h2 class="StepTitle">第一步 请勾选要编辑的字段</h2>
        <div>  
            <div style="width:200px;float:left;overflow:auto;" data-bind="autoheight:172">
                    <ul data-bind="easyuiTree:searchEdit.columntree"></ul>
            </div>  
            <div style="float:left;overflow:auto" data-bind="autoheight:172,autowidth:405">
                <table class="grid">
                    <thead>
                        <tr>
                            <th style="width:20%">字段</th>
                            <th style="width:40%">标签名称</th>
                            <th style="width:30%">控件类型</th>
                            <th style="width:10%">只读</th>
                        </tr>
                    </thead>
                    <tbody data-bind="foreach:form.conditions">
                        <tr data-bind="attr:{id:$index}">
                            <td data-bind="text:field" style="text-align:left"></td>
                            <td><input type="text" class="z-txt" data-bind="value:title"/></td>
                            <td><select class="z-txt"  data-bind="options:$root.data.input,value:type" style="width:60%"></select><a href="#">高级</a></td>
                            <td><input type="checkbox" data-bind="checked:readonly"/></td>
                        </tr>
                    </tbody>
                </table>
            </div>  
        </div>
    </div>
    <div id="step-2" class="step">
        <h2 class="StepTitle">第二步 请设置页面中的tab页签</h2>
         
        <div style="float:left;overflow:auto;width:150px;" data-bind="autoheight:172">
            <a href="#" class="buttonNext" style="float:left;margin:5px 3px 5px 0" data-bind="click:edit.addTab">添加Tab页签</a>
            <table class="grid">
                <thead>
                    <tr>
                        <th style="width:30%">#</th>
                        <th style="width:70%">名称</th>
                    </tr>
                </thead>
                <tbody data-bind="foreach:form.tabs">
                    <tr data-bind="attr:{id:$index}">
                        <td><a href="#" data-bind="click:$parent.edit.removeTab">删除</a></td>
                        <td><input type="text" class="z-txt" data-bind="value:title,click:$parent.edit.clickTab" style="width:90%"/></td>
                    </tr>
                </tbody>
            </table>
        </div>  

         <div id="edit-tab-setting" style="float:left;overflow:auto;" data-bind="autoheight:172,autowidth:355,visible:edit.selectedTitle()!=null">
 
        </div> 
    </div>

     <div id="step-3" class="step">
        <h2 class="StepTitle">第三步 其它设置</h2>

        <div class="container_12">
            <div class="grid_1 lbl">业务区域</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.area"/></div>

            <div class="clear"></div>

            <div class="grid_1 lbl">控制器名称</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.controller"/></div>

            <div class="clear"></div>

            <div class="grid_1 lbl">生成路径</div>
            <div class="grid_2 val"><input type="text" class="z-txt" data-bind="value:form.path"/></div>
        </div>
    </div>
</script>

<script type="text/html" id="template-edit-tab-setting">
     <div style="padding:8px;clear:both">
        <span>页签类型 </span>
        <select class="z-txt" style="padding:0;height:22px;" data-bind="value:edit.selectedTab.type">
            <option value="empty">empty</option>
            <option value="grid">grid</option>
            <option value="form">form</option>
        </select> 

        <span data-bind="visible:edit.selectedTab.type()!='empty'"> 数据表 </span>
        <select class="z-txt" style="padding:0;height:22px;" data-bind="options:data.table,optionsText:'text',optionsValue:'id',value:edit.selectedTab.subtable,visible:edit.selectedTab.type()!='empty'"></select>

        <span data-bind="visible:edit.selectedTab.type()!='empty'">与主表的关联</span>
        <select class="z-txt" style="padding:0;height:22px;" data-bind="options:data.tablekey,value:edit.selectedTab.relationship,visible:edit.selectedTab.type()!='empty'"></select>
    </div>

    <div style="width:180px;float:left;overflow:auto;margin-right:-18px;" data-bind="autoheight:212,visible:edit.selectedTab.type()!='empty'">
        <ul data-bind="easyuiTree:edit.columntree2"></ul>
    </div> 
    
    <div style="float:right;overflow:auto;" data-bind="autoheight:210,autowidth:535,visible:edit.selectedTab.type()!='empty'">
        <table class="grid">
            <thead>
                <tr>
                    <th style="width:50px">字段</th>
                    <th style="width:100px">题头</th>
                    <th style="width:30px" data-bind="visible:edit.selectedTab.type()=='grid'">隐藏</th>
                    <th style="width:30px" data-bind="visible:edit.selectedTab.type()=='grid'">排序</th>
                    <th style="width:50px" data-bind="visible:edit.selectedTab.type()=='grid'">对齐</th>
                    <th style="width:40px" data-bind="visible:edit.selectedTab.type()=='grid'">宽度</th>
                    <th style="width:50px" data-bind="visible:edit.selectedTab.type()=='grid'">格式化</th>
                    <th style="width:50px" data-bind="visible:edit.selectedTab.type()=='grid'">编辑器</th>
                    <th style="width:50px" data-bind="visible:edit.selectedTab.type()=='form'">控件类型</th>
                    <th style="width:10px" data-bind="visible:edit.selectedTab.type()=='form'">只读</th>
                </tr>
            </thead>
            <tbody data-bind="foreach:edit.selectedTab.columns">
                <tr data-bind="attr:{id:$index}">
                    <td data-bind="text:field" style="text-align:left"></td>
                    <td><input type="text" class="z-txt" data-bind="value:title" /></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='grid'"><input type="checkbox" data-bind="checked:hidden"/></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='grid'"><input type="checkbox" data-bind="checked:sortable"/></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='grid'"><select class="z-txt"  data-bind="options:$root.data.align,value:align" ></select></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='grid'"><input type="text" class="z-txt" data-bind="value:width" /></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='grid'"><select class="z-txt"  data-bind="options:$root.data.formatter,optionsText:'text',optionsValue:'value',value:formatter" ></select></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='grid'"><select class="z-txt"  data-bind="options:$root.data.editor,optionsText:'text',optionsValue:'value',value:editor" ></select></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='form'"><select class="z-txt"  data-bind="options:$root.data.input,value:type"></select></td>
                    <td data-bind="visible:$parent.edit.selectedTab.type()=='form'"><input type="checkbox" data-bind="checked:readonly"/></td>
                </tr>
            </tbody>
        </table>
    </div>
</script>
复制代码

Generator.js

复制代码
/**
* 模块名:mms viewModel
* 程序名: Generator.js
* Copyright(c) 2013 liuhuisheng [ liuhuisheng.xm@gmail.com ] 
**/

var viewModel = function () {
    var self = this;

    this.form = {
        type: '',
        database:ko.observable(),
        table: ko.observable(),
        controller: ko.observable(),
        area:ko.observable(),
        conditions: ko.observableArray(),
        columns: ko.observableArray(),
        tabs: ko.observableArray(),
        path: ko.observable("~/Generator/")
    };
 
    this.resetForm = function () {
        self.form.conditions([]);
        self.form.columns([]);
        self.form.tabs([]);
    };

    this.data = {
        codetype: [{ text: 'search', value: 'search' }, { text: 'edit', value: 'edit' }, { text: 'searchEdit', value: 'searchEdit' }],
        database: ko.observableArray(), 
        table: ko.observableArray(),
        column:ko.observableArray(),
        tablekey: ko.observableArray(),
        input: ['text', 'autocomplete', 'combobox', 'lookup','datebox','daterange'],
        compare: ['equal', 'like', 'startwith', 'endwith', 'greater', 'less', 'daterange'],
        align:['left','center','right'],
        formatter: [{text:'',value:''},{ text: '日期', value: 'com.formatDate' }, { text: '时间', value: 'com.formatTime' }, { text: '金额', value: 'com.formatMoney' }, { text: '是否', value: 'com.formatCheckbox' }],
        editor: [{text:'',value:''},{ text: '文本', value: 'text'}, { text: '整数', value: "{type: 'numberbox',options:{min: 0}}" }, { text: '两位小数', value: "{type: 'numberbox',options:{min: 0, precision: 2}}" }, { text: '下拉框', value: "{type:'combobox',options:{}}" }, { text: '弹出框', value: "{type:'lookup',options:{}}" }, { text: '日期', value: 'datebox' }]
    };

    this.initDatabase = function () {
        com.ajax({
            type: 'GET',
            async:false,
            url: '/api/sys/generator/GetConnectionStrings',
            success: function (d) {
                self.data.database(d);
            }
        });
    };

    this.initDatabase();

    this.getTableUrl = function () {
        return '/api/sys/generator/GetTables?database=' + self.form.database();
    };
    this.getColumnUrl = function (table) {
        return '/api/sys/generator/GetColumns?database=' + self.form.database() + "&table=" + table;
    }

    this.codetype = {
        showblank: true,
        width: 110,
        data: self.data.codetype,
        onSelect: function (node) {
            self.form.type = node.value;
            self.initWizard();
        }
    };

    this.database = {
        showblank: true,
        width: 110,
        data: self.data.database,
        onSelect: function (node) {
            self.form.database(node.value)
            self.form.area((node.value.split('.')[1] || node.value).replace(/(^|\s+)\w/g, function (s) { return s.toUpperCase(); }));
        }
    };

    this.tabletree = {
        method: 'GET',
        url: ko.computed(self.getTableUrl),
        loadFilter: function (d) {
            var data = utils.filterProperties(d.rows || d, ['TableName as id', 'TableName as text']);
            self.data.table(data);
            return data;
        },
        onSelect: function (node) {
            self.form.table(node.id);
            self.edit.init();
            self.resetWizard();
            self.form.controller((node.id.split('_')[1] || node.id).replace(/(^|\s+)\w/g, function (s) { return s.toUpperCase(); }));
        }
    };

    this.generator = function () {
        com.ajax({
            type:'POST',
            url: '/api/sys/generator',
            data: ko.toJSON(self.form),
            success: function (d) {
                com.message('success', "代码已生成!");
            }
        });
    };

    this.searchEdit = {};
    this.searchEdit.columntree = {
        method: 'GET',
        url: ko.computed(function () {
            return self.getColumnUrl(self.form.table());
        }),
        checkbox: true,
        loadFilter: function (d) {
            return utils.filterProperties(d.rows || d, ['ColumnName as id', 'ColumnName as text']);
        },
        onSelect: function (node) {
            var handle = node.checked ? 'uncheck' : 'check';
            $(this).tree(handle, node.target);
        },
        onCheck: function (node, checked) {
            if (checked)
                self.form.conditions.push({ field: node.id, title: node.id, type: 'text', options: '', cp: 'equal',readonly:false });
            else
                self.form.conditions.remove(function (item) { return item.field == node.id });
        },
        onLoadSuccess: self.resetForm
    };

    this.searchEdit.columntree2 = {
        method: 'GET',
        url: ko.computed(function () {
            return self.getColumnUrl(self.form.table());
        }),
        checkbox: true,
        loadFilter: function (d) {
            return utils.filterProperties(d.rows || d, ['ColumnName as id', 'ColumnName as text']);
        },
        onSelect: function (node) {
            var handle = node.checked ? 'uncheck' : 'check';
            $(this).tree(handle, node.target);
        },
        onCheck: function (node, checked) {
            var arr = self.form.columns;
            
            if (checked) {
                var item = $.grep(arr(), function (row) {return row.field == node.id;})[0];
                item || arr.push({ field: node.id, title: node.id, hidden: false, sortable: true, align: 'left', width: 80, formatter: '', editor: 'text' });
            } else
                arr.remove(function (item) { return item.field == node.id });
        }
    };

    this.edit = {};
    this.edit.selectedTab = {
        title: ko.observable(),
        type: ko.observable(),
        subtable: ko.observable(),
        relationship: ko.observable(),
        columns: ko.observableArray(),
        primaryKeys:ko.observableArray()
    };
     
    this.edit.columntree2 = {
        method: 'GET',
        url:ko.observable(),
        checkbox: true,
        loadFilter: function (d) {
            self.data.column(d);
            var list = utils.filterProperties(d.rows || d, ['ColumnName as id', 'ColumnName as text']);
            self.edit.setDefaultForm();
            self.edit.resetTableKey();
            var checkedList = [];
            for (var i in self.edit.selectedTab.columns())
                checkedList.push(self.edit.selectedTab.columns()[i].field);
            for (var i in list)
                if ($.inArray(list[i].id, checkedList) > -1) list[i].checked = true;
            
            return list
        },
        onSelect: function (node) {
            var handle = node.checked ? 'uncheck' : 'check';
            $(this).tree(handle, node.target);
        },
        onCheck: function (node, checked) {
            var arr = self.edit.selectedTab.columns;

            if (checked) {
                var item = $.grep(arr(), function (row) { return row.field == node.id; })[0];
                item || arr.push({ field: node.id, title: node.id, hidden: false, sortable: true, align: 'left', width: 80, formatter: '', editor: 'text', type: '', readonly: true });
            } else
                arr.remove(function (item) { return item.field == node.id });
        }
    }
    this.edit.init = function () {
        self.edit.selectedTitle(null);
        self.edit.selectedTab = null;
        $('#edit-tab-setting').empty();
    };
    this.edit.addTab = function () {
        var title = 'tab' + (self.form.tabs().length + 1);
        var newTab = {
            title: ko.observable(title),
            type: ko.observable('empty'),
            subtable: ko.observable(self.form.table()),
            relationship: ko.observable(),
            columns: ko.observableArray(),
            primaryKeys:ko.observableArray()
        };
        newTab.type.subscribe(function (value) {
            if (value == 'grid') {
                var item = $.grep(self.data.table(), function (row) { return row.id == self.form.table() + "Detail" })[0];
                if (item)
                    newTab.subtable(item.id);
            }
            else if (value == 'form') {
                newTab.subtable(self.form.table());
            }
        });
        newTab.columns.subscribe(self.tableDnDUpdate);
        newTab.subtable.subscribe(function (value) {
            self.edit.selectedTab.columns([]);
            self.edit.columntree2.url(self.getColumnUrl(value));
        });

        self.form.tabs.push(newTab);
    };
    
    this.edit.removeTab = function (row,event) {
        self.form.tabs.remove(row);

        if (row.title() == self.edit.selectedTitle())
            self.edit.selectedTitle(null);
    };
    this.edit.selectedTitle = ko.observable();
    this.edit.clickTab = function (row, event) {
        if (row.title() == self.edit.selectedTitle()) return;
 
        self.edit.selectedTitle(row.title());
        self.edit.selectedTab = row;
        self.edit.columntree2.url = ko.observable(self.getColumnUrl(self.edit.selectedTab.subtable()));

        var currentTr = $(event.srcElement).parent("td").parent("tr");
        currentTr.parent().find("tr.tree-node-selected").removeClass("tree-node-selected");
        currentTr.addClass("tree-node-selected");

        var tabTemplate = $('#template-edit-tab-setting').html();
        var wrapper = $('#edit-tab-setting').empty().html(tabTemplate);

        ko.cleanNode(wrapper[0]);
        ko.applyBindings(self, wrapper[0]);
        wrapper.find("table").tableDnD({ onDrop: self.tableDnDSort });
    };
    this.edit.resetTableKey = function () {
        var relationship = self.edit.selectedTab.relationship();
        self.data.tablekey([]);
        var cols = self.data.column();
        for (var i in cols)
            if (cols[i].IsIdentity || cols[i].IsPrimaryKey)
                self.data.tablekey.push(cols[i].ColumnName);

        self.edit.selectedTab.relationship(relationship);
        self.edit.selectedTab.primaryKeys(self.data.tablekey());
    };
    this.edit.setDefaultForm = function () {
        var arr = [
            { field: 'ApproveState', title: '审批状态', type: 'text', readonly: true },
            { field: 'ApproveRemark', title: '审批意见', type: 'text', readonly: true },
            { field: 'ApprovePerson', title: '审批人', type: 'text', readonly: true },
            { field: 'ApproveDate', title: '审批日期', type: 'datebox', readonly: true },
            { field: 'CreatePerson', title: '编制人', type: 'text', readonly: true },
            { field: 'CreateDate', title: '编制日期', type: 'datebox', readonly: true },
            { field: 'UpdatePerson', title: '修改人', type: 'text', readonly: true },
            { field: 'UpdateDate', title: '修改日期', type: 'datebox', readonly: true }
        ];

        var cols = self.data.column();
        var defaults = { field: '', title: '', hidden: false, sortable: true, align: 'left', width: 80, formatter: '', editor: 'text', type: '', readonly: true };
        for (var i in arr) {
            if (!$.grep(cols, function (item) { return item.ColumnName == arr[i].field; }).length)
                return;

            arr[i] = $.extend({}, defaults, arr[i]);
        }

        self.edit.selectedTab.columns(arr);

        var tree = self.edit.columntree2.$element();
        for (var i in arr) {
            var node = tree.tree('find', arr[i].field);
            if (node) tree.tree('check', node.target);
        }
    };

    this.initWizard = function () {
        var stepTemplate = $('#template-' + self.form.type);
        if (!stepTemplate.length) return;

        var wizard = $('#wizard').removeData('smartWizard').empty();
        ko.cleanNode(wizard[0]);
        
        wizard.html(stepTemplate.html());
        wizard.smartWizard({
            labelNext: '下一步',
            labelPrevious: '上一步',
            labelFinish: '生成',
            onFinish: self.generator
        });
        var resizeStep = function () {
            $(".step").height($(window).height() - 145)
                      .width($(window).width() - 205);
            $(".actionBar").width($(window).width() - 195);
            var index = wizard.smartWizard('currentStep');
            wizard.smartWizard('goToStep', index);
        };
        $(window).resize(resizeStep);
        resizeStep();
        ko.applyBindings(self, wizard[0]);
        wizard.find("table").tableDnD({ onDrop: self.tableDnDSort });

        for (var i in self.form) {
            if ($.isFunction(self.form[i]))
                if (self.form[i]() instanceof Array)
                    if (self.form[i].subscribe) 
                        self.form[i].subscribe(self.tableDnDUpdate);
        }
    };
    this.resetWizard = function () {
        var wizard = $("#wizard").smartWizard('goToStep', 1);
        for (var i = 1; i <= wizard.find(">ul>li").length; i++)
            wizard.smartWizard("disableStep", i);
    };

    this.tableDnDUpdate = function () {
        setTimeout('$("table").tableDnDUpdate()', 300);
    };
   
    this.tableDnDSort = function (table, row) {
        var name = $(table).find("tbody").attr("data-bind").replace('foreach:form.','');
        var array = self.form[name], i = 0;

        if (name == 'foreach:edit.selectedTab.columns')
            array = self.edit.selectedTab.columns;

        $("tr[id]", table).each(function () { array()[this.id].id = i++; });
        array.sort(function (left, right) { return left.id == right.id ? 0 : (left.id < right.id ? -1 : 1) });

        //for fix ko bug refresh ui
        var tempArr = array();
        array([]);
        array(tempArr);
    };
};
复制代码

razor模板
model.cshtml

复制代码
@using Zephyr.Core.Generator
using System;
using System.Collections.Generic;
using System.Text;
using Zephyr.Core;

namespace Zephyr.Web.@(@Model.Area).Models
{
    [Module("@Model.Database")]
    public class @(Model.TableName)Service : ServiceBase<@Model.TableName>
    {
       
    }

    public class @Model.TableName : ModelBase
    {
@foreach(TableSchema item in Model.Columns)
{
    if (item.IsIdentity)
    {
        @:[Identity]
    }

    if (item.IsPrimaryKey)
    {
        @:[PrimaryKey]   
    }
        @:public @item.TypeName @item.ColumnName { get; set; }
}
    }
}
复制代码

其它各页面的模板我就不一一贴出来了,大家可以查看我以前的那些关于共通viewModel的博客,查询及编辑页面我都有详细介绍过,大家也可以自己提炼出自己的一套模板,然后也可以用这同一个思路去做一个代码生成器。

六、代码生成用户界面

这个用户界面我们还是要把三种页面的定义分开看:

1、查询页面生成

第一步,选择代码类别search(查询页面),选择数据库,选择业务主表,再勾选字段即可实现查询条件部的设置,并且实现了拖拉排序功能。大家可以对照查询模板看。
image

第二步,选择grid中要显示的列,并且设置属性,格式化等
image

第三步,设置一些全局设定,主要根据这些参数确定命名空间,生成文件名等信息
image

点击生成按钮,按设定生成代码,生成后弹出文件夹,已分别生成MVC三层代码
image

mms_receive.cs
image

Index.cshtml
image

ReceiveController.cs
image

把这个代码直接拷贝到项目中直接运行,测试条件过滤都没有问题,grid会自适应高度,grid远程排序,选择分页翻页都没有问题,所有的功能都可用,
只有lookup控件弹出是空值,因为只把控制设置为了lookup但没有为它设置更详细的选项。autocomplete也是同样
即代码生成器已经生成了一个大的结构及UI,一些小细节还是要手动修改下,代码生成的UI界面如果把每个控件的选项也做进去会相当的复杂,也没有必要再细化了。
image

2、编辑页面生成
第一步,选择主表编辑区的字段及控件类型,控件类型中的高级还未实现,这个编辑的UI也可以参照编辑的模板看
image

第二步,添加tab页签,选择页签类型(grid,form,empty) grid是指跟主表N:1关系,form是指跟主表1:1关系,empty是空页签,生成后自己可以添加内容
这里我随便添加三个tab页签tab1 tab2 tab3
tab1用来放人员变动grid(跟主表关系N:1)image

tab2就选择form(跟主表关系1:1,也可以是主表本身)image

tab3也随便添些东西
image

第三步,其它设置
image

点击生成按钮,生成后自动打开文件夹
image

把这些代码拷贝到项目中直接运行
image

tab2
image

tab3,修改主表数据,tab1,tab2,tab3点保存,能保存成功,
image

审核按钮也可用,审核后单据不可修改
image

这个编辑功能基本上可以囊括很多的录入页面了,可以算是比较通用了

3、查询编辑页面(查询编辑在同一个页面内)页面生成

第一步,选择查询条件并设置控件类型image

第二步,设置grid中的数据列,及编辑器
image

第三步,其它设置
image

点击生成按钮,生成后自动打开文件夹
image

把代码直接拷贝到项目中运行,结果如下,经测试除了控件还需要进一步设置,所有按钮功能正常使用
image

 

七、后述

有了这个代码生成的功能,5分钟做一个基本的页面应该是完全没有问题的。
我这里分享下自己的代码生成的思路,权当抛砖引玉,大家有什么更好的方法欢迎留言。
如果大家感兴趣,就在右下角帮我【推荐】一下吧,谢谢大家了。

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(五)框架及Web项目的组件化 - 萧秦 - 博客园

mikel阅读(1415)

来源: 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(五)框架及Web项目的组件化 – 萧秦 – 博客园

一、组件化印象

1、先给大家看一张截图

image

如果我告诉大家,这就是一个web管理系统发布后的所有内容,你们会不会觉得太简洁了,只有一个web.config、一个Global.asax文件,其它的都是dll文件,没有aspx、cshtml、html页面,没有js css images文件,但它的确能跑起来,跑起来之后的截图如下

image

2、再看我新建一个项目的流程

新建一个ASP.NET MVC 4的空的项目

image

image

点确定之后,创建项目,默认的项目目录结构如下

image

然后打开nuget包管理界面,选择我们需要安装的组件 Zephyr.Web.Sys,这是我们权限管理系统

image

点安装进行安装,实现上就是引入dll文件

image

安装完成后,直接F5运行,就出现了以下页面

image

点创建后

image

我们选择MySQL输入相应的数据库信息测试连接通过后,点创建

image

创建成功

image

现在登录跳转到登陆页面

image

登录成功后

image

权限系统中页面测试都没有问题

image

然后我们回过头看程序,什么都没改变,

image

只是增加了一些引用及web.config中添加了一些信息。

image

那回过头来,这样发布后,不就是我们之前所看到了什么都没有的Web应用程序了

image

大家是不是也是很感兴趣,我当时也只是一个想法,但直正驱动我去完成这些功能的还是以下几点:

a 代码的重用性提高,可以很方便的用NuGet管理类库或系统。即使不用NuGet管理,直接把dll文件拷贝到新建项目的bin下面,然后修改下web.config也是可以的。比以前我们引用项目的方式方便很多。

b 创建一个新项目时只需要安装组件即可

c 发布后的程序干净,项目更新只需覆盖对应的dll即可。

d 更方便做成产品发布

 

二、NuGet操作

那么我们要做到前面所看到的,我们要做哪些工作?

1、准备好要组件化的的类库

image

2、创建Nuget包

创建NuGet包有很多种方法,我采用的是编译时自动生成NuGet包,

首先要启用NuGet包还原功能(Eable NuGet Package Restore),然后VS会自动下载添加一个.nuget的文件夹

image

然后编辑项目工程文件,添加最后一行<BuildPackage>true</BuildPackage>

image

然后编译后就会在bin目录下发现一个后缀为nupkg的文件,这就是我们要的nuget包

image

当然你也可以在程序包管理器控制台输入命令行进行打包
也可以下载一个图形化的打包器nuget package explorer

3、推送到NuGet服务上

你可以在https://www.nuget.org/注册上帐号,放在nuget官网上
也可以自己搭建一个NuGet服务器:新建一个空项目,安装NuGet.Server然后直接发布就可以了。具体教程网上也很多了。

上传NuGet包只需要执行以下命令
nuget push Zephyr.Core.2.0.0.0.nupkg -s http://192.168.1.100:8888

到这里就已经ok了,你可以在Nuget包管理界面中添加一个程序包源服务地址,然后就可以安装你刚刚上传的包了

 

三、打包Web应用会碰到的问题

1、资源的嵌入

资源的嵌入分两种,一种是静态资源,直接内嵌到程序集中
第二种是动态的页面,可以预编译成类放到程序集中

image

 

2、资源的访问

获取嵌入资源的访问地址

public static string GetResourceUrl(Type type string embedFileName)
{
var page = new Page();
    return page.ClientScript.GetWebResourceUrl(type, embedFileName);
}

当然你也可以通过controller去处理

复制代码
[AllowAnonymous]
[MvcMenuFilter(false)]
[WebFrameworkFilter]
public class ResourceController : Controller
{
    public ActionResult Index()
    {
        var assemblyName = ResourceVirtualPathProvider.ResourceVirtualPath.GetAssemblyName(Request.Path);
        var filename = ResourceVirtualPathProvider.ResourceVirtualPath.GetResourceName(Request.Path);
        var assembly = ReflectionHelper.GetAssembly(assemblyName);

        var stream = ResourceHelper.GetFileStream(assembly,filename);
        var contentType = ResourceHelper.GetContentType(filename);

        if (stream == null)
            return HttpNotFound();

        return File(stream, contentType);
    }
}
复制代码

四、框架组件化操作

我的框架Zephyr.Net主要分为:

类库 描述 依赖
Zephyr.Utils 工具类库
Zephyr.Library 常用其它类库
Zephyr.Data 数据库访问组件
Zephyr.Core 框架核心类 Zephyr.Utils Zephyr.Library Zephyr.Data
Zephyr.Web.Resource 静态资源
Zephyr.Web.Mvc 纵云Mvc框架 Zephyr.Core
Zephyr.Web.Sys 权限管理系统 Zephyr.Web.Mvc

以上是七个独立的项目,依赖关系也不直接引用项目,而是从nuget上引用发布程序包,

其中前面四个是类库
Zephyr.Web.Resource是嵌入静态资源的程序集,如果不引入这个文件,直接把静态资源文件夹拷贝进项目中也是可以的。
Zephyr.Web.Mvc是我们的Mvc框架基础,已经包括了mvc及web api很多的路由注册过滤器设定等设置,并且有Login页面和Index页面,安装了这个组件,即使不安装纵云权限系统组件,程序也一样可以跑起来,只是没有权限系统,是个空的架子。
Zephyr.Web.Sys就是权限系统了,把权限系统的mvc及webapi控制器、View页面、js以及数据库初始化处理都放在这个程序集中。
以后我们开发新的项目也可以做成跟这个权限系统一样,都放在一个dll中(当然也可以分多个,把controller model等都分成不同的层),以后只要引入或者bin下面放入这个dll就会动态引入这个模块,非常方便。

打包成nuget packages发布到nuget服务上,大功就告成了,如果有更新,只需要更新版本号重新发布就行了

image

这样一来,基本就把我的框架以及权限系统都做成组件了,如果新项目需要引入,或者多个项目升级都很方便了。

.NET框架交流群(三)  21549700

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(四)授权代码维护 - 萧秦 - 博客园

mikel阅读(964)

来源: 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(四)授权代码维护 – 萧秦 – 博客园

一、前言
权限系统设计中,授权代码是用来控制数据访问权限的。授权代码说白了只是一树型结构的数据,没有什么其它的业务意义。那么这个页面的功能也就非常简单授权代码维护:新增、修改、删除授权代码数据。

二、正文
我们实际上就是要实现一个treegrid的增删改的功能,技术上很容易实现。
1、新建控制器 PermissionControlle.cs

复制代码
public class PermissionController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}
复制代码

mvc控制器中不需要写任何的代码,就这样就ok

2、创建view

复制代码
@{
  ViewBag.Title = "授权代码";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

@section scripts{
    <script src="~/Areas/Sys/ViewModels/Permission.js"></script>
    <script type="text/javascript">
using(['combotree'],easyuifix.datagrid_editor_extend); 
        ko.bindingViewModel(new viewModel());
        var formatterParent = function (value, row) { return row.ParentName };
    </script>
}
    <div class="z-toolbar">
        <a id="a_refresh" href="#" plain="true" class="easyui-linkbutton" icon="icon-arrow_refresh" title="刷新" data-bind="click:refreshClick">刷新</a>
        <a id="a_add"  href="#" plain="true" class="easyui-linkbutton" icon="icon-add" title="新增" data-bind="click:addClick">新增</a>
        <a id="a_edit" href="#" plain="true" class="easyui-linkbutton" icon="icon-edit" data-bind="click:editClick" title="编辑">编辑</a>
        <a id="a_del"  href="#" plain="true" class="easyui-linkbutton" icon="icon-cross" title="删除" data-bind="click:deleteClick">删除</a>
        <a id="a_save" href="#" plain="true" class="easyui-linkbutton" icon="icon-save" data-bind="click:saveClick" title="保存">保存</a>
    </div>
    
    <table data-bind="treegrid:grid">
         <thead>  
            <tr>  
             <th field="_id"  hidden="true"></th>  
             <th field="PermissionName"  align="left" width="150" editor="{type:'validatebox',options:{required: true }}">授权名称   </th>  
             <th field="PermissionCode"  align="left" width="80"  editor="{type:'validatebox',options:{required: true }}">授权代码   </th>  
             <th field="ParentCode"      align="left" width="150" editor="combotree" formatter="formatterParent">上级授权   </th>
            </tr>                            
        </thead>      
    </table>
复制代码

这个view相对于其它页面来说也是相当的简洁了

3、前端的实现,主要是实现easyui treegrid的在线编辑功能及按钮的交互逻辑

复制代码
/**
* 模块名:mms viewModel
* 程序名: Permission.js
* Copyright(c) 2013-2015 liuhuisheng [ liuhuisheng.xm@gmail.com ] 
**/
 
function viewModel() {
    var self = this;
    this.grid = {
        size: { w: 4, h: 40 },
        url: '/api/sys/permission',
        idField: '_id',
        queryParams: ko.observable(),
        treeField: 'PermissionName',
        loadFilter: function (d) {
            d = utils.copyProperty(d.rows || d, ["PermissionCode"], ["_id"], false);
            return utils.toTreeData(d, '_id', 'ParentCode', "children");
        } 
    };
    this.refreshClick = function () {
        window.location.reload();
    };
    this.addClick = function () {
        if (self.grid.onClickRow()) {
            var row = { _id: utils.uuid(), PermissionCode: '', PermissionName: '' };
            self.grid.treegrid('append', { parent: '', data: [row] });
            self.grid.treegrid('select', row._id);
            self.grid.$element().data("datagrid").insertedRows.push(row);
            self.editClick();
        }
    };
    this.editClick = function () {
        var row = self.grid.treegrid('getSelected');
        if (row) {
            //取得父节点数据
            var treeData = JSON.parse(JSON.stringify(self.grid.treegrid('getData')).replace(/_id/g, "id").replace(/PermissionName/g, "text"));
            treeData.unshift({ "id": 0, "text": "" });

            //设置上级菜单下拉树
            var gridOpt = $.data(self.grid.$element()[0], "datagrid").options;
            var col = $.grep(gridOpt.columns[0], function (n) { return n.field == 'ParentCode' })[0];
            col.editor = { type: 'combotree', options: { data: treeData } };
            col.editor.options.onBeforeSelect = function (node) {
                var isChild = utils.isInChild(treeData, row._id, node.id);
                com.messageif(isChild, 'warning', '不能将自己或下级设为上级节点');
                return !isChild;
            };

            //开始编辑行数据
            self.grid.treegrid('beginEdit', row._id);
            self.edit_id = row._id;
        }
    };
   
    this.grid.OnBeforeDestroyEditor = function (editors, row) {
        row.ParentName = editors['ParentCode'].target.combotree('getText');
    };
    this.deleteClick = function () {
        var row = self.grid.treegrid('getSelected');
        if (row) {
            self.grid.$element().treegrid('remove', row._id);
            self.grid.$element().data("datagrid").deletedRows.push(row);
        }
    };
    this.grid.onDblClickRow = self.editClick;
    this.grid.onClickRow = function () {
        var edit_id = self.edit_id;
        if (!!edit_id) {
            if (self.grid.treegrid('validateRow', edit_id)) { //通过验证
                self.grid.treegrid('endEdit', edit_id);
                self.edit_id = undefined;
            }
            else { //未通过验证
                self.grid.treegrid('select', edit_id);
                return false;
            }
        }
        return true;
    };
    this.saveClick = function () {
        self.grid.onClickRow();
        var post = {};
        post.list = new com.editTreeGridViewModel(self.grid).getChanges(['_id', 'PermissionCode', 'PermissionName', 'ParentCode']);
        if (self.grid.onClickRow() && post.list._changed) {
            com.ajax({
                url: '/api/sys/permission/edit',
                data: ko.toJSON(post),
                success: function (d) {
                    com.message('success', '保存成功!');
                    self.grid.treegrid('acceptChanges');
                    self.grid.queryParams({});
                }
            });
        }

    };
}
复制代码

4、现在大家看我的东西都觉得是在看前端文章,实际上我在后台框架也做的很强大。现在重点放在后台,请大家注意我后端的写法,我们现在看web api中的处理。我这里用到了两个web api
1、取得授权代码数据:                                            GET    /api/sys/permission
2、保存treegrid中的修改(包括新增、修改、删除的数据)   POST  /api/sys/permission

大家注意下WebApi实现,超级简洁的代码实现

复制代码
public class PermissionApiController : ApiController
{
    //创建数据服务实例
    sys_permissionService service = new sys_permissionService();
    
    public IEnumerable<dynamic> Get()
    {
        //构建查询参数
        var pQuery = ParamQuery.Instance()
            .Select("A.*,B.PermissionName as ParentName")
            .From(@"sys_permission A left join sys_permission B on B.PermissionCode = A.ParentCode");

        //调用服务基类中的共通方法返回查询结果
        return service.GetDynamicList(pQuery);
    }
 
    [HttpPost]
    public void Edit(dynamic data)
    {
        //构建编辑的参数 传入的数据结构为data={deleted:[...],inserted:[...],updated:[...]}; 
        var listWrapper = RequestWrapper.Instance().LoadSettingXmlString(@"
<settings>
<table>sys_permission</table>
<where>
    <field name='PermissionCode' cp='equal' variable='_id'></field>
</where>
</settings>");

       //调用服务基类中的共通方法处理保存
        service.Edit(null, listWrapper, data);
    }
}
复制代码

以上的两个方法就已经实现了全部的功能了,这里我们好像觉得都是调用service中的方法,那我们再看看service类

public class sys_permissionService : ServiceBase<sys_permission>
{
       
}

这个数据服务类是空的,没有任何方法,只是继承了我的service基类,拥有了基类中定义的方法。
这里再简单的介绍下我的服务基类,只要服务类继承了服务基类后,就拥有以下方法

复制代码
//服务构造函数
public ServiceBase();
public ServiceBase(string moduleName);

//查询方法:
public List<dynamic> GetDynamicList(ParamQuery param = null);     //取得动态数据列表
public dynamic GetDynamicListWithPaging(ParamQuery param = null); //取得动态数据列表(带分页)

public List<T> GetModelList(ParamQuery param = null);             //取得Model列表数据
public dynamic GetModelListWithPaging(ParamQuery param = null);   //取得Model列表数据(带分页)
     
public T GetModel(ParamQuery param);                              //取得单model数据
public dynamic GetDynamic(ParamQuery param);                      //取得动态对象

public TField GetField<TField>(ParamQuery param);                 //取得字段的值
 
//数据插入:提供数据插入前后事件定义
protected virtual bool OnBeforeInsert(InsertEventArgs arg);
public int Insert(ParamInsert param);
protected virtual void OnAfterInsert(InsertEventArgs arg);

//数据更新:
protected virtual bool OnBeforeUpdate(UpdateEventArgs arg);
public int Update(ParamUpdate param);
protected virtual void OnAfterUpdate(UpdateEventArgs arg);

//数据删除:
protected virtual bool OnBeforeDelete(DeleteEventArgs arg);
public int Delete(ParamDelete param);
protected virtual void OnAfterDelete(DeleteEventArgs arg);

//存储过程执行:
public int StoredProcedure(ParamSP param);

//数据编辑(主从表在同一个事务中保存):
protected virtual bool OnBeforEdit(EditEventArgs arg);
protected virtual bool OnBeforEditMaster(EditEventArgs arg);
protected virtual bool OnBeforEditDetail(EditEventArgs arg);
public int Edit(RequestWrapper formWrapper, RequestWrapper listWrapper, JObject data);
protected virtual void OnAfterEdit(EditEventArgs arg);
protected virtual void OnAfterEditMaster(EditEventArgs arg);
protected virtual void OnAfterEditDetail(EditEventArgs arg);
复制代码

查询、新增、修改、删除、存储过程等每一个对象我都对应一个参数构造器,查询对应的是ParamQuery,每一个写法都可以像linq一样,比较方简洁。
好了,回过头再看看我的webapi中的代码,是不是每个方法只有两行代码,而且已经实现了很复杂的操作。
正是得利于我框架的这一点,才把我从后台中解放出来,有更多的时间精力去研究前端。

三、效果图
简单的几句代码就搞定了这个功能,我们来看看实现的效果
image

新增、修改、删除测试都ok

image

 

五、后述
如果你觉得不错就帮我【推荐】一下吧,你的支持才是我能坚持写完这个系列文章的动力。
技术交流QQ群:群一:328510073(已满),群二:167813846,欢迎大家来交流。

系列博客链接:

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(三)图形化机构树

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(二)菜单导航

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(一)

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(三)图形化机构树 - 萧秦 - 博客园

mikel阅读(1359)

来源: 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(三)图形化机构树 – 萧秦 – 博客园

一、前言

组织机构是国内管理系统中很重要的一个概念,以前我们基本都是采用数据列表的形式展现,最多只是采用树形列表展现。虽然够用,但是如果能做成图形化当然是最好不过了。这里我不用任何图形控件,就用最原始的方式,用脚本画html的方式来展现一个图形化的机构树。

二、功能分析

当然我们除了生成图形的功能还有其它的维护机构数据的功能:
1、展现机构图形
2、新增组织机构
3、编辑组织机构
4、删除组织机构
5、给组织机构设置拥有的角色

三、具体实现

图形展示的实现前面已经说了用脚本画页面html,
新增修改节点则利用easyui的window或dialog控件弹出窗口编辑
设置角色也弹出窗口选择,除了展现图形其它的应该都没什么难度,后台还是采用webapi来处理数据。

1、还是从mvc控制器开始,新建一个名叫Organize的控制器,代码如下:

复制代码
    public class OrganizeController : Controller
    {
        public ActionResult Index()
        {
            var model = new sys_organizeService().GetModelList();
            return View(model);
        }
    }
复制代码

这里直接把机构数据取出来传到view中使用,当然也可以在前台脚本中ajax请求到webapi中获得。

2、接下来再创建对应的view

复制代码
@{
    ViewBag.Title = "title";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section scripts{
  @Scripts.Render("~/Resource/Sys/Organize.js")
  
  <script type="text/javascript">
      var data = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model));
      ko.bindingViewModel(new viewModelOrganize(data));
  </script>
}
  <div class="z-toolbar">
      <a id="a_refresh" href="#" plain="true" class="easyui-linkbutton" icon="icon-rfs" title="刷新" data-bind="click:refreshClick">刷新</a>
      <a id="a_add"    href="#" plain="true" class="easyui-linkbutton" icon="icon-add" title="新增" data-bind="click:addClick">新增</a>
      <a id="a_edit"   href="#" plain="true" class="easyui-linkbutton" icon="icon-edit" data-bind="click:editClick" title="编辑">编辑</a>
      <a id="a_del"    href="#" plain="true" class="easyui-linkbutton" icon="icon-cross" title="删除" data-bind="click:deleteClick">删除</a>
      <a id="a_role"   href="#" plain="true" class="easyui-linkbutton" icon="icon-group" title="设置角色" data-bind="click:roleClick">设置角色</a>
  </div>
  
  <div class="wrapper" style="width: 100%; height: 100%; margin-top:15px;"></div>

<script type="text/html" id="edit-template">
    <div class="container_16" style="width:90%;margin:5%;">  
        <div class="grid_3 lbl" >上级机构</div>  
        <div class="grid_13 val" ><input class="z-text easyui-combotree" data-bind="datasource:combotreeData,combotreeValue:form.ParentCode" /><span data-bind="text:form.ParentCode" style="margin:5px;"></span></div>
        <div class="grid_3 lbl">机构编码</div>  
        <div class="grid_13 val"><input class="z-txt easyui-validatebox" style="width:145px;" data-bind="value:form.OrganizeCode" data-options="required:true" /></div>
        <div class="grid_3 lbl">机构名称</div>  
        <div class="grid_13 val"><input class="z-txt easyui-validatebox" style="width:145px;" data-bind="value:form.OrganizeName" data-options="required:true" /></div>
        <div class="grid_3 lbl">备注</div>  
        <div class="grid_13 val"><textarea class="z-txt" style="width:220px;height:50px;" data-bind="value:form.Description"  ></textarea></div>
        <div class="clear"></div>
    </div> 
    <div style="text-align:center;">
        <a class="easyui-linkbutton" data-options="iconCls:'icon-ok'" data-bind="click:confirmClick" href="javascript:void(0)"  >确定</a>  
        <a class="easyui-linkbutton" data-options="iconCls:'icon-cancel'" data-bind="click:cancelClick" href="javascript:void(0)">取消</a> 
    </div>
</script>

<script type="text/html" id="setrole-template">
    <style type="text/css">
        .listview{ margin:0 !important;}
        .listview li{width:100px !important;background-color:skyblue !important;float:left;margin:3px;}
    </style>
    <div style="margin:5px;height:370px;overflow:auto;"  >
        <div style="border-bottom:1px solid #CCC; margin-bottom:5px;">
            <span class="icon32 icon-org32" style="padding-left:48px;font-weight:bold; font-size:14px;color:#666;" data-bind="text:OrganizeName">机构名称</span> 
        </div>
        <div> 拥有角色(请点击勾选):</div>
        <div class="metrouicss">
            <ul class="listview"></ul>
        </div>
    </div>
    <div style="text-align:center;">
        <a class="easyui-linkbutton" data-options="iconCls:'icon-ok'" data-bind="click:confirmClick" href="javascript:void(0)"  >确定</a>  
        <a class="easyui-linkbutton" data-options="iconCls:'icon-cancel'" data-bind="click:cancelClick" href="javascript:void(0)">取消</a> 
    </div>
</script>

<script type="text/html" id="tr-node-template">
    <tr class="tr-node"><td colspan="{0}">
       <table align="center"border="1" cellpadding="2" cellspacing="0">
          <tr>
              <td class="td-node" id='td{3}' data-node='{2}' align="center" valign="top">{1}</td>
          </tr>
      </table>
    </td></tr>
</script>

 <script type="text/html" id="tr-hline-template">
     <tr class="tr-hline">
        <td><table><tr><td class="treeempty"></td><td class="treedot"></td><td class="treedot"></td></tr></table></td>
        <td class="treedot" colspan="{0}"></td>
        <td><table><tr><td class="treedot"></td><td class="treedot"></td><td class="treempty"></td></tr></table></td>
    </tr>
</script>
复制代码

这个view还是很简单的,图形区只需要一个class=”wrapper”的div即可,其它都是html模板,弹出窗口及生成图形时用到。

3、前端UI与数据交互的viewModel

复制代码
/**
* 模块名:mms viewModel
* 程序名: organize.js
* Copyright(c) 2013-2015 liuhuisheng [ liuhuisheng.xm@gmail.com ] 
**/

function viewModelOrganize(data) {
    var self = this;
    this.refreshClick = function () {
        window.location.reload();
    };
    this.save = function (vm,win) {
        var post = { form: ko.toJS(vm.form) };
        com.ajax({
            type: 'POST',
            url: '/api/sys/organize/edit',
            data: JSON.stringify(post),
            success: function (d) {
                com.message('success', '保存成功!');
                win.dialog('close');
                self.initGraph(d);
            }
        });
    }
    this.addClick = function () {
        var defaults = { ParentCode: (self.selectNode || {}).OrganizeCode || 0 };
        self.openDiloag("添加新机构", defaults, function (vm, win) {
            if (com.formValidate(win)) {
                vm.form._OrganizeCode = vm.form.OrganizeCode();
                self.save(vm,win);
            }
        });
    };
    this.editClick = function () {
        if (!self.selectNode) return com.message('warning', '请先选择一个机构!');
        self.openDiloag("编辑机构-"+self.selectNode.OrganizeName,self.selectNode, function (vm, win) {
            if (com.formValidate(win)) {
                self.save(vm,win);
            }
        });
    };
    this.deleteClick = function () {
        if (!self.selectNode) return com.message('warning', '请先选择一个机构!');
        com.message('confirm', '确认要删除选中的机构吗?', function (b) {
            if (b) {
                com.ajax({
                    type: 'DELETE',
                    url: '/api/sys/organize/' + self.selectNode.OrganizeCode,
                    success: function (d) {
                        com.message('success', '删除成功!');
                        self.initGraph(d);
                    }
                });
            }
        });
    };
    this.roleClick = function () {
        if (!self.selectNode)
            return com.message('warning', '请先选择一个机构!');
        com.dialog({
            title: "设置角色",
            width: 600,
            height: 450,
            html: "#setrole-template",
            viewModel: function (w) {
                var thisRole = this;
                this.OrganizeName = ko.observable(self.selectNode.OrganizeName);
                com.loadCss('/Resource/css/metro/css/modern.css', parent.document);
                com.ajax({
                    type: 'GET',
                    url: '/api/sys/organize/getrolewithorganizecheck/' + self.selectNode.OrganizeCode,
                    success: function (d) {
                        var ul = w.find(".listview");
                        for (var i in d)
                            ul.append(utils.formatString('<li role="{0}" class="{2}">{1}</li>', d[i].RoleCode, d[i].RoleName, d[i].Checked == 'true' ? 'selected' : ''));
                        ul.find("li").click(function () {
                            if ($(this).hasClass('selected'))
                                $(this).removeClass('selected');
                            else
                                $(this).addClass('selected');
                        });
                    }
                });
                this.confirmClick = function () {
                    var roles = [];
                    w.find("li.selected").each(function () {
                        roles.push({ RoleCode: $(this).attr('role') });
                    });
                    com.ajax({
                        url: '/api/sys/organize/editorganizeroles/' + self.selectNode.OrganizeCode,
                        data: ko.toJSON(roles),
                        success: function (d) {
                            thisRole.cancelClick();
                            com.message('success', '保存成功!');
                        }
                    });
                };
                this.cancelClick = function () {
                    w.dialog('close');
                };
            }
        });
    };
    this.openDiloag = function (title,node,fnConfirm) {
        com.dialog({
            title: title,
            height: 250,
            width: 400,
            html: "#edit-template",
            viewModel: function (w) {
                var that = this;
                this.combotreeData = function () {
                    var list = utils.filterProperties(data, ['OrganizeCode as id', 'ParentCode as pid', 'OrganizeName as text']);
                    var treeData = utils.toTreeData(list, "id", "pid", "children");
                    treeData.unshift({ "id": 0, "text": "==请选择==" });
                    return treeData;
                };
                this.form = {
                    _OrganizeCode:node.OrganizeCode,
                    ParentCode: ko.observable(node.ParentCode),
                    OrganizeCode: ko.observable(node.OrganizeCode),
                    OrganizeName: ko.observable(node.OrganizeName),
                    Description: ko.observable(node.Description)
                };
                this.calcCode = function (v) { //新增时 自动计算OrganizeCode
                    if (!that.form._OrganizeCode) {
                        v = v == 0 ? "" : v;
                        var list = [], suffix;
                        for (var i in self.data) {
                            list.push(self.data[i].OrganizeCode);
                        }
                        for (var j = 1; j < 100; j++) {
                            suffix = j < 10 ? ("0" + j.toString()) : j.toString();
                            if ($.inArray(v + suffix,list) == -1)  
                                break;
                        }
                        that.form.OrganizeCode(v + suffix);
                    }
                };

                this.form.ParentCode.subscribe(this.calcCode);
                this.calcCode(node.ParentCode);

                this.confirmClick = function () {
                    fnConfirm(this,w);
                };
                this.cancelClick = function () {
                    w.dialog('close');
                };
            }
        });
    };
    this.initGraph = function (data) {
        self.data = data;
        var wrapper = $("div.wrapper").empty();
        var treeData = utils.toTreeData(data, "OrganizeCode", "ParentCode", "children");

        var tb = renderTreeGraph(treeData);
        tb.appendTo(wrapper);
 
        //绑定事件
        $(wrapper).find(".td-node").click(function () {
            $(".td-node").css({ "background-color": "#f6f6ff", "color": "" });
            $(this).css({ "background-color": "#faffbe", "color": "#FF0000" });
            self.selectNode = $(this).data("node");
        }).dblclick(self.editClick);
        if (self.selectNode) {
            $("#td" + self.selectNode.OrganizeCode).css({ "background-color": "#faffbe", "color": "#FF0000" });
        }
    };
    this.initGraph(data);
}

function renderTreeGraph(treeData) {
    //生成图形
    var tb = $('<table class="tb-node" cellspacing="0" cellpadding="0" align="center" border="0" style="border-width:0px;border-collapse:collapse;margin:0 auto;vertical-align:top"></table>');
    var tr = $('<tr></tr>');
    for (var i in treeData) {
        if (i > 0) $('<td>&nbsp;</td>').appendTo(tr);
        $('<td style="vertical-align:top;text-align:center;"></td>').append(createChild(treeData[i])).appendTo(tr);
    }
    tr.appendTo(tb);
    return tb;
}
 
//递归生成机构树图形
function createChild(node, ischild) {
    var length = (node.children || []).length;
    var colspan = length * 2 - 1;
    if (length == 0)
        colspan = 1;

    var fnTrVert = function () {
        var tr1 = $('<tr class="tr-vline"><td colspan="'+colspan+'"><img class="img-v" src="/Resource/images/tree/Tree_Vert.gif" ></td></tr>');
        return tr1;
    };
    //1.创建容器
    var tb = $('<table class="tb-node" cellspacing="0" cellpadding="0" align="center" border="0"></table>');

    //2.如果本节点是子节点,添加竖线在节点上面
    if (ischild) {
        fnTrVert().appendTo(tb);
    }

    // 3.添加本节点到图表
    var tr3 = $("#tr-node-template").html();
    tr3 = utils.formatString(tr3, colspan, node.OrganizeName, JSON.stringify(node),node.OrganizeCode);
    $(tr3).appendTo(tb);

    // 4.增加上下级的连接线
    if (length > 1) {
        //增加本级连接下级的首节点竖线,在节点下方
        fnTrVert().appendTo(tb);

        //增加本级连接下级的中间横线
        var tr4 = $("#tr-hline-template").html();
        tr4 = utils.formatString(tr4, colspan - 2);
        $(tr4).appendTo(tb);
    }

    //5.递归增加下级所有子节点到图表
    if (length > 0) {
        var tr5 = $('<tr></tr>');

        for (var i in node.children) {
            if (i > 0) {
                $('<td</td>').appendTo(tr5);
            }
            $('<td></td>').append(createChild(node.children[i], true)).appendTo(tr5);
        }

        tr5.appendTo(tb);
    }

    return tb;
}
复制代码

这段交互的逻辑和之前的viewModel一样,基本上定义了工具栏上的按钮对应的事件,通过data-bind绑定到UI上。熟悉knockoutjs的朋友就很容易理解了。
在新增机构时我有做一个处理根据新增节点的父节点自动计算本节点的编码。
生成图形的处理基本上是通过生成table来配合一些背景图片来实现图形展示,我注释写得很清楚了。

4、后台webapi中的数据处理
主要是viewModel中ajax调用的方法,有以下:
1 添加或编辑机构       POST     /api/sys/organize/edit
2 删除机构               DELETE  /api/sys/organize/id
3 获取机构拥有的角色 GET       /api/sys/organize/getrolewithorganizecheck/id
4 保存机构拥有的角色 POST     /api/sys/organize/editorganizeroles/id
那么我们的web api controller中:

复制代码
    public class OrganizeApiController : ApiController
    {
        public dynamic GetRoleWithOrganizeCheck(string id)
        {
            var service = new sys_organizeService();
            return service.GetOrganizeRole(id);
        }

        [System.Web.Http.HttpPost]
        public dynamic Edit(dynamic data)
        {
            var formWrapper = RequestWrapper.Instance().LoadSettingXmlString(@"
<settings>
    <table>
        sys_organize
    </table>
    <where>
        <field name='OrganizeCode' cp='equal' variable='_OrganizeCode'></field>
    </where>
</settings>");
            var service = new sys_organizeService();
            service.Edit(formWrapper, null, data);
var result = service.GetModelList();
            return result;
        }

        public dynamic Delete(string id)
        {
            var service = new sys_organizeService();
            service.RecursionDelete(id);
            var result = service.GetModelList();
            return result;
        }

        [System.Web.Http.HttpPost]
        public void EditOrganizeRoles(string id, dynamic data)
        {
            var service = new sys_organizeService();
            service.SaveOrganizeRoles(id, data as JToken);
        }
    }
复制代码

在webapi中后台的处理很简单,每个方法只有几句代码。至此我们已经大功告成了。

四、效果图

 

打开页面
image

选择总务后点击新增时,会自动把总务设置为父节点,并计算出新的机构编码
image

保存成功
image

双击修改总务节点image

给总务节点设置角色
image

五、后述
由于一些私事,这段时间一直都没去办公室,今晚偷空写了这篇,感谢大家对我的支持。
这个系列的博客写了后很多博友问我要这个框架的源码,我们打算让大家团了,300人起团,人数到了就统一发给大家。

如果你觉得不错就帮我【推荐】一下吧,你的支持才是我能坚持写完这个系列文章的动力。
技术交流QQ群:群一:328510073(已满),群二:167813846,欢迎大家来交流,想团源码的朋友进群后把城市-名字-手机-QQ发给我即可。

系列博客链接:

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(一)
我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(二)菜单导航