ASP.NET CORE小试牛刀:干货及完整源代码

发表于:2017-7-20 09:44

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:我叫So    来源:博客

  扯淡
  .NET Core 的推出让开发者欣喜万分,从封闭到拥抱开源十分振奋人心。对跨平台的支持,也让咱.NET开发者体验了一把 Write once,run any where 的感觉!近期离职后,时间比较充裕,我也花了些时间学习了 ASP.NET Core 开发,并且成功将之前的一个小网站 www.52chloe.com 极其后台管理移植成 ASP.NET Core,并部署到 linux 上。项目完整源码已经提交到 github,感兴趣的可以看看,希望对大家有用。
  项目介绍
  前端以 MVVM 框架 knockout.js 为主,jQuery 为辅,css 使用 bootstrap。后端就是 ASP.NET Core + AutoMapper + Chloe.ORM,日志记录使用 NLog。整个项目结构如下: 
  常规的分层,简单介绍下各层: 
  Ace:项目架构基础层,里面包含了一些基础接口的定义,如应用服务接口,以及很多重用性高的代码。同时,我在这个文件夹下建了 Ace.Web 和 Ace.Web.Mvc 两个dll,分别是对 asp.net core 和 asp.net core mvc 的一些公共扩展和通用的方法。这一层里的东西,基本都是不跟任何业务挂钩重用度极高的代码,而且是比较方便移植的。 
  Application:应(业)用(务)服(逻)务(辑)层。不同模块业务逻辑可以放在不同的 dll 中。规范是 Ace.Application.{ModuleName},这样做的目的是隔离不同的功能模块代码,避免所有东西都塞在一个 dll 里。 
  Data:数据层。包含实体类和ORM操作有关的基础类。不同模块的实体同样可以放在不同的 dll 中。 
  Web:所谓的展示层。
  由于LZ个人对开发规范很在(洁)意(癖),多年来一直希望打造一个符合自己的代码规范。无论是写前端 js,还是后端 C#。这个项目.NET Framework版本的源码很早之前就放在 github 上,有一些看过源码的同学表示看不懂,所以,我也简单介绍下其中的一些设计思路及风格。
  前端freestyle
  做开发都知道,很多时候我们都是在写一些“雷同”的代码,特别是在做一些后台管理类的项目,基本都是 CRUD,一个功能需求来了,大多时候是将现有的代码拷贝一遍,改一下。除了这样貌似也没什么好办法,哈哈。既然避免不了拷贝粘贴,那我们就让我们要拷贝的代码和改动点尽量少吧。我们来分析下一个拥有标准 CRUD 的一个前端界面:
  其实,在一些项目中,与上图类似的界面不少。正常情况下,如果我们走拷贝粘贴然后修改的路子,会出现很多重复代码,比如图中各个按钮点击事件绑定,弹框逻辑等等,写多了会非常蛋疼。前面提到过,我们要将拷贝的代码和改动点尽量少!怎么办呢?继承和抽象!我们只要把“重复雷同”的代码放到一个基类里,每个页面的 ViewModel 继承这个基类就好了,开发的时候页面的 ViewModel 实现变动的逻辑即可 。ViewModelBase 如下:
  function ViewModelBase() {
      var me = this;
      me.SearchModel = _ob({});
      me.DeleteUrl = null;
      me.ModelKeyName = "Id"; /* 实体主键名称 */
      /* 如有必要,子类需重写 DataTable、Dialog */
      me.DataTable = new PagedDataTable(me);
      me.Dialog = new DialogBase();
      /* 添加按钮点击事件 */
      me.Add = function () {
          EnsureNotNull(me.Dialog, "Dialog");
          me.Dialog.Open(null, "添加");
      }
      /* 编辑按钮点击事件 */
      me.Edit = function () {
          EnsureNotNull(me.DataTable, "DataTable");
          EnsureNotNull(me.Dialog, "Dialog");
          me.Dialog.Open(me.DataTable.SelectedModel(), "修改");
      }
      /* 删除按钮点击事件 */
      me.Delete = function () {
          $ace.confirm("确定要删除该条数据吗?", me.OnDelete);
      }
      me.OnDelete = function () {
          DeleteRow();
      }
      /* 要求每行必须有 Id 属性,如果主键名不是 Id,则需要重写 me.ModelKeyName */
      function DeleteRow() {
          if (me.DeleteUrl == null)
              throw new Error("未指定 DeleteUrl");
          var url = me.DeleteUrl;
          var params = { id: me.DataTable.SelectedModel()[me.ModelKeyName]() };
          $ace.post(url, params, function (result) {
              var msg = result.Msg || "删除成功";
              $ace.msg(msg);
              me.DataTable.RemoveSelectedModel();
          });
      }
      /* 搜索按钮点击事件 */
      me.Search = function () {
          me.LoadModels();
      }
      /* 搜索数据逻辑,子类需要重写 */
      me.LoadModels = function () {
          throw new Error("未重写 LoadModels 方法");
      }
      function EnsureNotNull(obj, name) {
          if (!obj)
              throw new Error("属性 " + name + " 未初始化");
      }
  }
  ViewModelBase 拥有界面上通用的点击按钮事件函数:Add、Edit、Delete以及Search查询等。Search 方法是界面搜索按钮点击时调用的执行事件,内部调用 LoadModels 加载数据,因为每个页面的查询逻辑不同, LoadModels 是一个没有任何实现的方法,因此如果一个页面有搜索展示数据功能,直接实现该方法即可。这样,每个页面的 ViewModel 代码条理清晰、简洁:
  var _vm;
      $(function () {
          var vm = new ViewModel();
          _vm = vm;
          vmExtend.call(vm);/* 将 vmExtend 的成员扩展到 vm 对象上 */
          ko.applyBindings(vm);
          vm.Init();
      });
      function ViewModel() {
          var me = this;
          ViewModelBase.call(me);
          vmExtend.call(me);/* 实现继承 */
          me.DeleteUrl = "@this.Href("~/WikiManage/WikiMenu/Delete")";
          me.DataTable = new DataTableBase(me);
          me.Dialog = new Dialog(me);
          me.RootMenuItems = _oba(@this.RawSerialize( ViewBag.RootMenuItems));
          me.Documents = _oba(@this.RawSerialize(ViewBag.Documents));
      }
      /* ViewModel 的一些私有方法,这里面的成员会被扩展到 ViewModel 实例上 */
      function vmExtend() {
          var me = this;
          me.Init = function () {
              me.LoadModels();
          }
          /* 重写父类方法,加载数据,并绑定到页面表格上 */
          me.LoadModels = function () {
              me.DataTable.SelectedModel(null);
              var data = me.SearchModel();
              $ace.get("@this.Href("~/WikiManage/WikiMenu/GetModels")", data, function (result) {
                  me.DataTable.SetModels(result.Data);
              }
            );
          }
      }
      /* 模态框 */
      function Dialog(vm) {
          var me = this;
          DialogBase.call(me);
          /* 打开模态框时触发函数 */
          me.OnOpen = function () {
              var model = me.EditModel();
              if (model) {
                  var dataModel = model.Data;
                  var bindModel = $ko.toJS(dataModel);
                  me.Model(bindModel);
              }
              else {
                  me.EditModel(null);
                  me.Model({ IsEnabled: true });
              }
          }
          /* 点击保存按钮时保存表单逻辑 */
          me.OnSave = function () {
              var model = me.Model();
              if (!$('#form1').formValid()) {
                  return false;
              }
              if (me.EditModel()) {
                  $ace.post("@this.Href("~/WikiManage/WikiMenu/Update")", model, function (result) {
                      $ace.msg(result.Msg);
                      me.Close();
                      vm.LoadModels();
                  }
                 );
              }
              else {
                  $ace.post("@this.Href("~/WikiManage/WikiMenu/Add")", model, function (result) {
                      $ace.msg(result.Msg);
                      me.Close();
                      vm.LoadModels();
                      if (!result.Data.ParentId) {
                          vm.RootMenuItems.push(result.Data);
                      }
                  }
               );
              }
          }
      }
  注意上面代码:ViewModelBase.call(me); 这句代码会使是 ViewModel 类继承前面提到过的 ViewModelBase 基类(确切的说不叫继承,而是将一个类的成员扩展到另外一个类上),通过这种方式,我们就可以少写一些重复逻辑了。等等,ViewModel 里的 DataTable 和 Dialog 是干什么用的?哈哈,其实我是把界面的表格和模态框做了抽象。大家可以这样理解,Dialog 是属于 ViewModel 的,但是 Dialog 里的东西(如表单,保存和关闭按钮极其事件)是 Dialog 自身拥有的,这些其实也是重复通用的代码,都封装在 DialogBase 基类里,代码就不贴了,感兴趣的自个儿翻源码看就好,DataTable 同理。这应该也算是面向对象开发思想的基本运用吧。通过公共代码提取和抽象,开发一个新页面,我们只需要修改变动的逻辑即可。
  上述提到的 ViewModelBase 和 DialogBase 基类都会放在一个公共的 js 文件里,我们在页面中引用(布局页_LayoutPage里)。而 html 页面,我们只管绑定数据即可:
  @{
      ViewBag.Title = "Index";
      Layout = "~/Views/Shared/_LayoutPage.cshtml";
  }
  @this.Partial("Index-js")
  <div class="topPanel">
      <div class="toolbar">
          <div class="btn-group">
              <a class="btn btn-primary" onclick="$ace.reload()"><span class="glyphicon glyphicon-refresh"></span></a>
          </div>
          <div class="btn-group">
              <button class="btn btn-primary" data-bind="click:Edit,attr:{disabled:!DataTable.SelectedModel()}"><i class="fa fa-pencil-square-o"></i>修改菜单</button>
              <button class="btn btn-primary" data-bind="click:Delete,attr:{disabled:!DataTable.SelectedModel()}"><i class="fa fa-trash-o"></i>删除菜单</button>
              <button class="btn btn-primary" data-bind="click:Add"><i class="fa fa-plus"></i>新建菜单</button>
          </div>
      </div>
      <div class="search">
          <table>
              <tr>
                  <td>
                      <div class="input-group">
                          <input id="txt_keyword" type="text" class="form-control" placeholder="请输入要查询关键字" style="width: 200px;" data-bind="value:SearchModel().keyword">
                          <span class="input-group-btn">
                              <button id="btn_search" type="button" class="btn  btn-primary" data-bind="click:Search"><i class="fa fa-search"></i></button>
                          </span>
                      </div>
                  </td>
              </tr>
          </table>
      </div>
  </div>
  <!-- 页面数据 -->
  <div class="table-responsive">
      <table class="table table-hover" data-bind="with:DataTable">
          <thead>
              <tr>
                  <th style="width:20px;"></th>
                  <th>名称</th>
                  <th>文档</th>
                  <th>文档标签</th>
                  <th>是否显示</th>
                  <th>排序</th>
              </tr>
          </thead>
          <tbody data-bind="foreach:Models">
              <tr data-bind="click:$parent.SelectRow, attr: { id: $data.Id, 'parent-id': $data.ParentId }">
                  <td data-bind="text:$parent.GetOrdinal($index())"></td>
                  <td>
                      <!-- ko if: $data.HasChildren -->
                      <div onclick="expandChildren(this);" style="left:0px;cursor:pointer;" class="glyphicon glyphicon-triangle-bottom" data-bind=""></div>
                      <!-- /ko -->
                      <!-- ko if: !$data.HasChildren() -->
                      <div style="width:12px;height:12px;display:inline-block;"></div>
                      <!-- /ko -->
                      <span data-bind="html:appendRetract($data.Level())"></span>
                      <span data-bind="text:$data.Data.Name"></span>
                  </td>
                  <td>
                      <a href="#" target="_blank" data-bind="text:$ace.getOptionTextByValue($root.Documents(),$data.Data.DocumentId(),'Id','Title'),attr:{href:'@Url.Content("~/WikiManage/WikiDocument/Document?id=")' + $data.Data.DocumentId()}"></a>
                  </td>
                  <td data-bind="text:$ace.getOptionTextByValue($root.Documents(),$data.Data.DocumentId(),'Id','Tag')"></td>
                  <td data-bind="boolString:$data.Data.IsEnabled"></td>
                  <td data-bind="boolString:$data.Data.SortCode"></td>
              </tr>
          </tbody>
      </table>
  </div>
  <!-- 表单模态框 -->
  <dialogbox data-bind="with:Dialog">
      <form id="form1">
          <table class="form">
              <tr>
                  <td class="formTitle">上级</td>
                  <td class="formValue">
                      <select id="ParentId" name="ParentId" class="form-control" data-bind="options:$root.RootMenuItems,optionsText:'Name',optionsValue:'Id', optionsCaption:'-请选择-',value:Model().ParentId"></select>
                  </td>
                  <td class="formTitle">名称</td>
                  <td class="formValue">
                      <input id="Name" name="Name" type="text" class="form-control required" placeholder="请输入名称" data-bind="value:Model().Name" />
                  </td>
              </tr>
              <tr>
                  <td class="formTitle">文档</td>
                  <td class="formValue">
                      <select id="DocumentId" name="DocumentId" class="form-control" data-bind="options:$root.Documents,optionsText:'Title',optionsValue:'Id', optionsCaption:'-请选择-',value:Model().DocumentId"></select>
                  </td>
                  <td class="formTitle">是否显示</td>
                  <td class="formValue">
                      <label><input type="radio" name="IsEnabled" value="true" data-bind="typedChecked:Model().IsEnabled,dataType:'bool'" />是</label>
                      <label><input type="radio" name="IsEnabled" value="false" data-bind="typedChecked:Model().IsEnabled,dataType:'bool'" />否</label>
                  </td>
              </tr>
              <tr>
                  <td class="formTitle">排序</td>
                  <td class="formValue">
                      <input id="SortCode" name="SortCode" type="text" class="form-control" placeholder="请输入排序" data-bind="value:Model().SortCode" />
                  </td>
              </tr>
          </table>
      </form>
  </dialogbox>

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号