smartadmin.core.urf 這個項目是基於asp.net core 3.1(最新)基礎上參照領域驅動設計(DDD)的理念,並參考目前最爲了流行的abp架構開發的一套輕量級的快速開發web application 技術架構,專一業務核心需求,減小重複代碼,開始構建和發佈,讓初級程序員也能開發出專業而且漂亮的Web應用程序javascript
域驅動設計(DDD)是一種經過將實現與不斷髮展的模型相鏈接來知足複雜需求的軟件開發方法。域驅動設計的前提以下:css
- 將項目的主要重點放在覈心領域和領域邏輯上;
- 將複雜的設計基於領域模型;
- 啓動技術專家和領域專家之間的創造性合做,以迭代方式完善解決特定領域問題的概念模型。
最終的核心思想仍是SOLID,只是實現的方式有所不一樣,ABP可能目前對DDD設計理念最好的實現方式。但對於小項目我仍是更喜歡 URF.Core https://github.com/urfnet/URF.Core 這個超輕量級的實現。html
同時這個項目也就是我2年前的一個開源項目 ASP.NET MVC 5 SmartCode Scaffolding for Visual Studio.Net 的升級版,支持.net core.目前沒有把全部功能都遷移到.net core,其中最重要的就是代碼生成這塊。再接下來的時間裏主要就是完善代碼生成的插件。固然也要看是否受歡迎,若是反應通常,我可能不會繼續更新。java
演示站點
帳號:demo 密碼:123456jquery
GitHub 源代碼 https://github.com/neozhu/smartadmin.core.urfgit
喜歡請給個 Star 每一顆Star都是鼓勵我繼續更新的動力 謝謝
若是你用於本身公司及盈利性的項目,但願給與金錢上的贊助,而且保留原做者的版權程序員
smartadmin.core.urf遵行DDD設計模式來實現應用程序的四層模型github
域層(Domain Layer)web
- 在域層定義:本項目就是(SmartAdmin.Entity.csproj)
- 繼承一個基類 Entity,添加必要審計類好比:建立時間,最後修改時間等
- 必需要有一個主鍵最好是GRUID(不推薦複合主鍵),但本項目使用遞增的int類型
- 字段不要過多的冗餘,能夠經過定義關聯關係
- 字段屬性和方法儘可能使用virtual關鍵字。有些ORM和動態代理工具須要
應用層ajax
基礎服務層
- Visual Studio .Net 2019
- .Net Core 3.1
- Sql Server(LocalDb)
使用SQL Server Management Studio 附加.\src\SmartAdmin.Data\db\smartadmindb.mdf 數據庫(若是是localdb,那麼不須要修改數據庫鏈接配置)
第一個簡單的需求開始
新增 Company 企業信息 完成CRUD 導入導出功能
在SmartAdmin.Entity.csproj項目的Models目錄下新增一個Company.cs類
1 //記住:定義實體對象最佳作法,繼承基類,使用virtual關鍵字,儘量的定義每一個屬性,名稱,類型,長度,校驗規則,索引,默認值等 2 namespace SmartAdmin.Data.Models 3 { 4 public partial class Company : URF.Core.EF.Trackable.Entity 5 { 6 [Display(Name = "企業名稱", Description = "歸屬企業名稱")] 7 [MaxLength(50)] 8 [Required] 9 //[Index(IsUnique = true)] 10 public virtual string Name { get; set; } 11 [Display(Name = "組織代碼", Description = "組織代碼")] 12 [MaxLength(12)] 13 //[Index(IsUnique = true)] 14 [Required] 15 public virtual string Code { get; set; } 16 [Display(Name = "地址", Description = "地址")] 17 [MaxLength(128)] 18 [DefaultValue("-")] 19 public virtual string Address { get; set; } 20 [Display(Name = "聯繫人", Description = "聯繫人")] 21 [MaxLength(12)] 22 public virtual string Contect { get; set; } 23 [Display(Name = "聯繫電話", Description = "聯繫電話")] 24 [MaxLength(20)] 25 public virtual string PhoneNumber { get; set; } 26 [Display(Name = "註冊日期", Description = "註冊日期")] 27 [DefaultValue("now")] 28 public virtual DateTime RegisterDate { get; set; } 29 } 30 } 31 //在 SmartAdmin.Data.csproj 項目 SmartDbContext.cs 添加 32 public virtual DbSet<Company> Companies { get; set; }
在項目 SmartAdmin.Service.csproj 中添加ICompanyService.cs,CompanyService.cs 就是用來實現業務需求 用例的地方
1 //ICompany.cs 2 //根據實際業務用例來建立方法,默認的CRUD,增刪改查不須要再定義 3 namespace SmartAdmin.Service 4 { 5 // Example: extending IService<TEntity> and/or ITrackableRepository<TEntity>, scope: ICustomerService 6 public interface ICompanyService : IService<Company> 7 { 8 // Example: adding synchronous Single method, scope: ICustomerService 9 Company Single(Expression<Func<Company, bool>> predicate); 10 Task ImportDataTableAsync(DataTable datatable); 11 Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc"); 12 } 13 } 14 // 具體實現接口的方法 15 namespace SmartAdmin.Service 16 { 17 public class CompanyService : Service<Company>, ICompanyService 18 { 19 private readonly IDataTableImportMappingService mappingservice; 20 private readonly ILogger<CompanyService> logger; 21 public CompanyService( 22 IDataTableImportMappingService mappingservice, 23 ILogger<CompanyService> logger, 24 ITrackableRepository<Company> repository) : base(repository) 25 { 26 this.mappingservice = mappingservice; 27 this.logger = logger; 28 } 29 30 public async Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc") 31 { 32 var filters = PredicateBuilder.FromFilter<Company>(filterRules); 33 var expcolopts = await this.mappingservice.Queryable() 34 .Where(x => x.EntitySetName == "Company") 35 .Select(x => new ExpColumnOpts() 36 { 37 EntitySetName = x.EntitySetName, 38 FieldName = x.FieldName, 39 IgnoredColumn = x.IgnoredColumn, 40 SourceFieldName = x.SourceFieldName 41 }).ToArrayAsync(); 42 43 var works = (await this.Query(filters).OrderBy(n => n.OrderBy(sort, order)).SelectAsync()).ToList(); 44 var datarows = works.Select(n => new 45 { 46 Id = n.Id, 47 Name = n.Name, 48 Code = n.Code, 49 Address = n.Address, 50 Contect = n.Contect, 51 PhoneNumber = n.PhoneNumber, 52 RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss") 53 }).ToList(); 54 return await NPOIHelper.ExportExcelAsync("Company", datarows, expcolopts); 55 } 56 57 public async Task ImportDataTableAsync(DataTable datatable) 58 { 59 var mapping = await this.mappingservice.Queryable() 60 .Where(x => x.EntitySetName == "Company" && 61 (x.IsEnabled == true || (x.IsEnabled == false && x.DefaultValue != null)) 62 ).ToListAsync(); 63 if (mapping.Count == 0) 64 { 65 throw new NullReferenceException("沒有找到Work對象的Excel導入配置信息,請執行[系統管理/Excel導入配置]"); 66 } 67 foreach (DataRow row in datatable.Rows) 68 { 69 70 var requiredfield = mapping.Where(x => x.IsRequired == true && x.IsEnabled == true && x.DefaultValue == null).FirstOrDefault()?.SourceFieldName; 71 if (requiredfield != null || !row.IsNull(requiredfield)) 72 { 73 var item = new Company(); 74 foreach (var field in mapping) 75 { 76 var defval = field.DefaultValue; 77 var contain = datatable.Columns.Contains(field.SourceFieldName ?? ""); 78 if (contain && !row.IsNull(field.SourceFieldName)) 79 { 80 var worktype = item.GetType(); 81 var propertyInfo = worktype.GetProperty(field.FieldName); 82 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; 83 var safeValue = (row[field.SourceFieldName] == null) ? null : Convert.ChangeType(row[field.SourceFieldName], safetype); 84 propertyInfo.SetValue(item, safeValue, null); 85 } 86 else if (!string.IsNullOrEmpty(defval)) 87 { 88 var worktype = item.GetType(); 89 var propertyInfo = worktype.GetProperty(field.FieldName); 90 if (string.Equals(defval, "now", StringComparison.OrdinalIgnoreCase) && (propertyInfo.PropertyType == typeof(DateTime) || propertyInfo.PropertyType == typeof(Nullable<DateTime>))) 91 { 92 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; 93 var safeValue = Convert.ChangeType(DateTime.Now, safetype); 94 propertyInfo.SetValue(item, safeValue, null); 95 } 96 else if (string.Equals(defval, "guid", StringComparison.OrdinalIgnoreCase)) 97 { 98 propertyInfo.SetValue(item, Guid.NewGuid().ToString(), null); 99 } 100 else if (string.Equals(defval, "user", StringComparison.OrdinalIgnoreCase)) 101 { 102 propertyInfo.SetValue(item, "", null); 103 } 104 else 105 { 106 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; 107 var safeValue = Convert.ChangeType(defval, safetype); 108 propertyInfo.SetValue(item, safeValue, null); 109 } 110 } 111 } 112 this.Insert(item); 113 } 114 } 115 } 116 117 // Example, adding synchronous Single method 118 public Company Single(Expression<Func<Company, bool>> predicate) 119 { 120 121 return this.Repository.Queryable().Single(predicate); 122 123 } 124 } 125 }
MVC Controller
1 namespace SmartAdmin.WebUI.Controllers 2 { 3 public class CompaniesController : Controller 4 { 5 private readonly ICompanyService companyService; 6 private readonly IUnitOfWork unitOfWork; 7 private readonly ILogger<CompaniesController> _logger; 8 private readonly IWebHostEnvironment _webHostEnvironment; 9 public CompaniesController(ICompanyService companyService, 10 IUnitOfWork unitOfWork, 11 IWebHostEnvironment webHostEnvironment, 12 ILogger<CompaniesController> logger) 13 { 14 this.companyService = companyService; 15 this.unitOfWork = unitOfWork; 16 this._logger = logger; 17 this._webHostEnvironment = webHostEnvironment; 18 } 19 20 // GET: Companies 21 public IActionResult Index()=> View(); 22 //datagrid 數據源 23 public async Task<JsonResult> GetData(int page = 1, int rows = 10, string sort = "Id", string order = "asc", string filterRules = "") 24 { 25 try 26 { 27 var filters = PredicateBuilder.FromFilter<Company>(filterRules); 28 var total = await this.companyService 29 .Query(filters) 30 .AsNoTracking() 31 .CountAsync() 32 ; 33 var pagerows = (await this.companyService 34 .Query(filters) 35 .AsNoTracking() 36 .OrderBy(n => n.OrderBy(sort, order)) 37 .Skip(page - 1).Take(rows) 38 .SelectAsync()) 39 .Select(n => new 40 { 41 Id = n.Id, 42 Name = n.Name, 43 Code = n.Code, 44 Address = n.Address, 45 Contect = n.Contect, 46 PhoneNumber = n.PhoneNumber, 47 RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss") 48 }).ToList(); 49 var pagelist = new { total = total, rows = pagerows }; 50 return Json(pagelist); 51 } 52 catch(Exception e) { 53 throw e; 54 } 55 56 } 57 //編輯 58 [HttpPost] 59 [ValidateAntiForgeryToken] 60 public async Task<JsonResult> Edit(Company company) 61 { 62 if (ModelState.IsValid) 63 { 64 try 65 { 66 this.companyService.Update(company); 67 68 var result = await this.unitOfWork.SaveChangesAsync(); 69 return Json(new { success = true, result = result }); 70 } 71 catch (Exception e) 72 { 73 return Json(new { success = false, err = e.GetBaseException().Message }); 74 } 75 } 76 else 77 { 78 var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage))); 79 return Json(new { success = false, err = modelStateErrors }); 80 //DisplayErrorMessage(modelStateErrors); 81 } 82 //return View(work); 83 } 84 //新建 85 [HttpPost] 86 [ValidateAntiForgeryToken] 87 88 public async Task<JsonResult> Create([Bind("Name,Code,Address,Contect,PhoneNumber,RegisterDate")] Company company) 89 { 90 if (ModelState.IsValid) 91 { 92 try 93 { 94 this.companyService.Insert(company); 95 await this.unitOfWork.SaveChangesAsync(); 96 return Json(new { success = true}); 97 } 98 catch (Exception e) 99 { 100 return Json(new { success = false, err = e.GetBaseException().Message }); 101 } 102 103 //DisplaySuccessMessage("Has update a Work record"); 104 //return RedirectToAction("Index"); 105 } 106 else 107 { 108 var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage))); 109 return Json(new { success = false, err = modelStateErrors }); 110 //DisplayErrorMessage(modelStateErrors); 111 } 112 //return View(work); 113 } 114 //刪除當前記錄 115 //GET: Companies/Delete/:id 116 [HttpGet] 117 public async Task<JsonResult> Delete(int id) 118 { 119 try 120 { 121 await this.companyService.DeleteAsync(id); 122 await this.unitOfWork.SaveChangesAsync(); 123 return Json(new { success = true }); 124 } 125 126 catch (Exception e) 127 { 128 return Json(new { success = false, err = e.GetBaseException().Message }); 129 } 130 } 131 //刪除選中的記錄 132 [HttpPost] 133 public async Task<JsonResult> DeleteChecked(int[] id) 134 { 135 try 136 { 137 foreach (var key in id) 138 { 139 await this.companyService.DeleteAsync(key); 140 } 141 await this.unitOfWork.SaveChangesAsync(); 142 return Json(new { success = true }); 143 } 144 catch (Exception e) 145 { 146 return Json(new { success = false, err = e.GetBaseException().Message }); 147 } 148 } 149 //保存datagrid編輯的數據 150 [HttpPost] 151 public async Task<JsonResult> AcceptChanges(Company[] companies) 152 { 153 if (ModelState.IsValid) 154 { 155 try 156 { 157 foreach (var item in companies) 158 { 159 this.companyService.ApplyChanges(item); 160 } 161 var result = await this.unitOfWork.SaveChangesAsync(); 162 return Json(new { success = true, result }); 163 } 164 catch (Exception e) 165 { 166 return Json(new { success = false, err = e.GetBaseException().Message }); 167 } 168 } 169 else 170 { 171 var modelStateErrors = string.Join(",", ModelState.Keys.SelectMany(key => ModelState[key].Errors.Select(n => n.ErrorMessage))); 172 return Json(new { success = false, err = modelStateErrors }); 173 } 174 175 } 176 //導出Excel 177 [HttpPost] 178 public async Task<IActionResult> ExportExcel(string filterRules = "", string sort = "Id", string order = "asc") 179 { 180 var fileName = "compnay" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx"; 181 var stream = await this.companyService.ExportExcelAsync(filterRules, sort, order); 182 return File(stream, "application/vnd.ms-excel", fileName); 183 } 184 //導入excel 185 [HttpPost] 186 public async Task<IActionResult> ImportExcel() 187 { 188 try 189 { 190 var watch = new Stopwatch(); 191 watch.Start(); 192 var total = 0; 193 if (Request.Form.Files.Count > 0) 194 { 195 for (var i = 0; i < this.Request.Form.Files.Count; i++) 196 { 197 var model = Request.Form["model"].FirstOrDefault() ?? "company"; 198 var folder = Request.Form["folder"].FirstOrDefault() ?? "company"; 199 var autosave = Convert.ToBoolean(Request.Form["autosave"].FirstOrDefault()); 200 var properties = (Request.Form["properties"].FirstOrDefault()?.Split(',')); 201 var file = Request.Form.Files[i]; 202 var filename = file.FileName; 203 var contenttype = file.ContentType; 204 var size = file.Length; 205 var ext = Path.GetExtension(filename); 206 var path = Path.Combine(this._webHostEnvironment.ContentRootPath, "UploadFiles", folder); 207 if (!Directory.Exists(path)) 208 { 209 Directory.CreateDirectory(path); 210 } 211 var datatable = await NPOIHelper.GetDataTableFromExcelAsync(file.OpenReadStream(), ext); 212 await this.companyService.ImportDataTableAsync(datatable); 213 await this.unitOfWork.SaveChangesAsync(); 214 total = datatable.Rows.Count; 215 if (autosave) 216 { 217 var filepath = Path.Combine(path, filename); 218 file.OpenReadStream().Position = 0; 219 220 using (var stream = System.IO.File.Create(filepath)) 221 { 222 await file.CopyToAsync(stream); 223 } 224 } 225 226 } 227 } 228 watch.Stop(); 229 return Json(new { success = true, total = total, elapsedTime = watch.ElapsedMilliseconds }); 230 } 231 catch (Exception e) { 232 this._logger.LogError(e, "Excel導入失敗"); 233 return this.Json(new { success = false, err = e.GetBaseException().Message }); 234 } 235 } 236 //下載模板 237 public async Task<IActionResult> Download(string file) { 238 239 this.Response.Cookies.Append("fileDownload", "true"); 240 var path = Path.Combine(this._webHostEnvironment.ContentRootPath, file); 241 var downloadFile = new FileInfo(path); 242 if (downloadFile.Exists) 243 { 244 var fileName = downloadFile.Name; 245 var mimeType = MimeTypeConvert.FromExtension(downloadFile.Extension); 246 var fileContent = new byte[Convert.ToInt32(downloadFile.Length)]; 247 using (var fs = downloadFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) 248 { 249 await fs.ReadAsync(fileContent, 0, Convert.ToInt32(downloadFile.Length)); 250 } 251 return this.File(fileContent, mimeType, fileName); 252 } 253 else 254 { 255 throw new FileNotFoundException($"文件 {file} 不存在!"); 256 } 257 } 258 259 } 260 }
MVC Views\Companies\Index
1 @model SmartAdmin.Data.Models.Company 2 @{ 3 ViewData["Title"] = "企業信息"; 4 ViewData["PageName"] = "Companies_Index"; 5 ViewData["Heading"] = "<i class='fal fa-window text-primary'></i> 企業信息"; 6 ViewData["Category1"] = "組織架構"; 7 ViewData["PageDescription"] = ""; 8 } 9 <div class="row"> 10 <div class="col-lg-12 col-xl-12"> 11 <div id="panel-1" class="panel"> 12 <div class="panel-hdr"> 13 <h2> 14 公司信息 15 </h2> 16 <div class="panel-toolbar"> 17 <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-collapse" data-toggle="tooltip" data-offset="0,10" data-original-title="Collapse"><i class="fal fa-window-minimize"></i></button> 18 <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-fullscreen" data-toggle="tooltip" data-offset="0,10" data-original-title="Fullscreen"><i class="fal fa-expand"></i></button> 19 </div> 20 21 </div> 22 <div class="panel-container show"> 23 <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0 text-muted bg-subtlelight-fade "> 24 <div class="row no-gutters align-items-center"> 25 <div class="col"> 26 <!-- 開啓受權控制請參考 @@if (Html.IsAuthorize("Create") --> 27 <div class="btn-group btn-group-sm"> 28 <button onclick="append()" class="btn btn-default"> 29 <span class="fal fa-plus mr-1"></span> 新增 30 </button> 31 </div> 32 <div class="btn-group btn-group-sm"> 33 <button name="deletebutton" disabled onclick="removeit()" class="btn btn-default"> 34 <span class="fal fa-times mr-1"></span> 刪除 35 </button> 36 </div> 37 <div class="btn-group btn-group-sm"> 38 <button name="savebutton" disabled onclick="acceptChanges()" class="btn btn-default"> 39 <span class="fal fa-save mr-1"></span> 保存 40 </button> 41 </div> 42 <div class="btn-group btn-group-sm"> 43 <button name="cancelbutton" disabled onclick="rejectChanges()" class="btn btn-default"> 44 <span class="fal fa-ban mr-1"></span> 取消 45 </button> 46 </div> 47 <div class="btn-group btn-group-sm"> 48 <button onclick="reload()" class="btn btn-default"> <span class="fal fa-search mr-1"></span> 查詢 </button> 49 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 50 <span class="sr-only">Toggle Dropdown</span> 51 </button> 52 <div class="dropdown-menu dropdown-menu-animated"> 53 <a class="dropdown-item js-waves-on" href="javascript:void()"> 個人記錄 </a> 54 <div class="dropdown-divider"></div> 55 <a class="dropdown-item js-waves-on" href="javascript:void()"> 自定義查詢 </a> 56 </div> 57 </div> 58 <div class="btn-group btn-group-sm hidden-xs"> 59 <button type="button" onclick="importExcel.upload()" class="btn btn-default"><span class="fal fa-cloud-upload mr-1"></span> 導入 </button> 60 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split waves-effect waves-themed" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 61 <span class="sr-only">Toggle Dropdown</span> 62 </button> 63 <div class="dropdown-menu dropdown-menu-animated"> 64 <a class="dropdown-item js-waves-on" href="javascript:importExcel.downloadtemplate()"> 65 <span class="fal fa-download"></span> 下載模板 66 </a> 67 </div> 68 </div> 69 <div class="btn-group btn-group-sm hidden-xs"> 70 <button onclick="exportexcel()" class="btn btn-default"> 71 <span class="fal fa-file-export mr-1"></span> 導出 72 </button> 73 </div> 74 75 </div> 76 77 </div> 78 79 </div> 80 <div class="panel-content"> 81 <div class="table-responsive"> 82 <table id="companies_datagrid"> 83 </table> 84 </div> 85 </div> 86 </div> 87 </div> 88 </div> 89 </div> 90 <!-- 彈出窗體form表單 --> 91 <div id="companydetailwindow" class="easyui-window" 92 title="明細數據" 93 data-options="modal:true, 94 closed:true, 95 minimizable:false, 96 collapsible:false, 97 maximized:false, 98 iconCls:'fal fa-window', 99 onBeforeClose:function(){ 100 var that = $(this); 101 if(companyhasmodified()){ 102 $.messager.confirm('確認','你肯定要放棄保存修改的記錄?',function(r){ 103 if (r){ 104 var opts = that.panel('options'); 105 var onBeforeClose = opts.onBeforeClose; 106 opts.onBeforeClose = function(){}; 107 that.panel('close'); 108 opts.onBeforeClose = onBeforeClose; 109 hook = false; 110 } 111 }); 112 return false; 113 } 114 }, 115 onOpen:function(){ 116 $(this).window('vcenter'); 117 $(this).window('hcenter'); 118 }, 119 onRestore:function(){ 120 }, 121 onMaximize:function(){ 122 } 123 " style="width:820px;height:420px;display:none"> 124 <!-- toolbar --> 125 <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0 text-muted bg-subtlelight-fade sticky-top"> 126 <div class="d-flex flex-row-reverse pr-4"> 127 <div class="btn-group btn-group-sm mr-1"> 128 <button name="saveitembutton" onclick="savecompanyitem()" class="btn btn-default"> 129 <i class="fal fa-save"></i> 保存 130 </button> 131 </div> 132 <div class="btn-group btn-group-sm mr-1" id="deleteitem-btn-group"> 133 <button onclick="deletecompanyitem()" class="btn btn-danger"> 134 <i class="fal fa-trash-alt"></i> 刪除 135 </button> 136 </div> 137 </div> 138 </div> 139 <div class="panel-container show"> 140 <div class="container"> 141 <div class="panel-content"> 142 <form id="company_form" 143 class="easyui-form form-horizontal p-1" 144 method="post" 145 data-options="novalidate:true, 146 onChange: function(target){ 147 hook = true; 148 $('button[name*=\'saveitembutton\']').prop('disabled', false); 149 }, 150 onLoadSuccess:function(data){ 151 hook = false; 152 $('button[name*=\'saveitembutton\']').prop('disabled', true); 153 }"> 154 @Html.AntiForgeryToken() 155 <!--Primary Key--> 156 @Html.HiddenFor(model => model.Id) 157 <fieldset class="form-group"> 158 <!-- begin row --> 159 <!--名稱--> 160 <div class="row h-100 justify-content-center align-items-center"> 161 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Name)</label> 162 <div class="col-md-4 mb-1 pl-1"> 163 <input id="@Html.IdFor(model => model.Name)" 164 name="@Html.NameFor(model => model.Name)" 165 value="@Html.ValueFor(model => model.Name)" 166 tabindex="0" required 167 class="easyui-textbox" 168 style="width:100%" 169 type="text" 170 data-options="prompt:'@Html.DescriptionFor(model => model.Name)', 171 required:true, 172 validType: 'length[0,50]' 173 " /> 174 </div> 175 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Code)</label> 176 <div class="col-md-4 mb-1 pl-1"> 177 <input id="@Html.IdFor(model => model.Code)" 178 name="@Html.NameFor(model => model.Code)" 179 value="@Html.ValueFor(model => model.Code)" 180 tabindex="1" required 181 class="easyui-textbox" 182 style="width:100%" 183 type="text" 184 data-options="prompt:'@Html.DescriptionFor(model => model.Code)', 185 required:true, 186 validType: 'length[0,12]' 187 " /> 188 </div> 189 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Address)</label> 190 <div class="col-md-4 mb-1 pl-1"> 191 <input id="@Html.IdFor(model => model.Address)" 192 name="@Html.NameFor(model => model.Address)" 193 value="@Html.ValueFor(model => model.Address)" 194 tabindex="2" 195 class="easyui-textbox" 196 style="width:100%" 197 type="text" 198 data-options="prompt:'@Html.DescriptionFor(model => model.Address)', 199 required:false, 200 validType: 'length[0,50]' 201 " /> 202 </div> 203 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Contect)</label> 204 <div class="col-md-4 mb-1 pl-1"> 205 <input id="@Html.IdFor(model => model.Contect)" 206 name="@Html.NameFor(model => model.Contect)" 207 value="@Html.ValueFor(model => model.Contect)" 208 tabindex="3" 209 class="easyui-textbox" 210 style="width:100%" 211 type="text" 212 data-options="prompt:'@Html.DescriptionFor(model => model.Contect)', 213 required:false, 214 validType: 'length[0,12]' 215 " /> 216 </div> 217 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.PhoneNumber)</label> 218 <div class="col-md-4 mb-1 pl-1"> 219 <input id="@Html.IdFor(model => model.PhoneNumber)" 220 name="@Html.NameFor(model => model.PhoneNumber)" 221 value="@Html.ValueFor(model => model.PhoneNumber)" 222 tabindex="4" 223 class="easyui-textbox" 224 style="width:100%" 225 type="text" 226 data-options="prompt:'@Html.DescriptionFor(model => model.PhoneNumber)', 227 required:false, 228 validType: 'length[0,20]' 229 " /> 230 </div> 231 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.RegisterDate)</label> 232 <div class="col-md-4 mb-1 pl-1"> 233 <input id="@Html.IdFor(model => model.RegisterDate)" 234 name="@Html.NameFor(model => model.RegisterDate)" 235 value="@Html.ValueFor(model => model.RegisterDate)" 236 tabindex="5" required 237 class="easyui-datebox" 238 style="width:100%" 239 type="text" 240 data-options="prompt:'@Html.DescriptionFor(model => model.RegisterDate)', 241 required:true, 242 formatter:dateformatter" /> 243 </div> 244 </div> 245 </fieldset> 246 </form> 247 </div> 248 </div> 249 </div> 250 </div> 251 252 253 @await Component.InvokeAsync("ImportExcel", new ImportExcelOptions { entity="Company", 254 folder="Companies", 255 url="/Companies/ImportExcel", 256 tpl="/Companies/Download" 257 258 259 }) 260 261 @section HeadBlock { 262 <link href="~/css/notifications/toastr/toastr.css" rel="stylesheet" asp-append-version="true" /> 263 <link href="~/css/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.css" rel="stylesheet" asp-append-version="true" /> 264 <link href="~/js/easyui/themes/insdep/easyui.css" rel="stylesheet" asp-append-version="true" /> 265 } 266 @section ScriptsBlock { 267 <script src="~/js/dependency/moment/moment.js" asp-append-version="true"></script> 268 <script src="~/js/notifications/toastr/toastr.js"></script> 269 <script src="~/js/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.js" asp-append-version="true"></script> 270 <script src="~/js/easyui/jquery.easyui.min.js" asp-append-version="true"></script> 271 <script src="~/js/easyui/plugins/datagrid-filter.js" asp-append-version="true"></script> 272 <script src="~/js/easyui/plugins/columns-ext.js" asp-append-version="true"></script> 273 <script src="~/js/easyui/plugins/columns-reset.js" asp-append-version="true"></script> 274 <script src="~/js/easyui/locale/easyui-lang-zh_CN.js" asp-append-version="true"></script> 275 <script src="~/js/easyui/jquery.easyui.component.js" asp-append-version="true"></script> 276 <script src="~/js/plugin/filesaver/FileSaver.js" asp-append-version="true"></script> 277 <script src="~/js/plugin/jquery.serializejson/jquery.serializejson.js" asp-append-version="true"></script> 278 <script src="~/js/jquery.custom.extend.js" asp-append-version="true"></script> 279 <script src="~/js/jquery.extend.formatter.js" asp-append-version="true"></script> 280 <script> 281 var $dg = $('#companies_datagrid'); 282 var EDITINLINE = true; 283 var company = null; 284 var editIndex = undefined; 285 //下載Excel導入模板 286 287 //執行導出下載Excel 288 function exportexcel() { 289 const filterRules = JSON.stringify($dg.datagrid('options').filterRules); 290 console.log(filterRules); 291 $.messager.progress({ title: '請等待',msg:'正在執行導出...' }); 292 let formData = new FormData(); 293 formData.append('filterRules', filterRules); 294 formData.append('sort', 'Id'); 295 formData.append('order', 'asc'); 296 $.postDownload('/Companies/ExportExcel', formData).then(res => { 297 $.messager.progress('close'); 298 toastr.success('導出成功!'); 299 }).catch(err => { 300 //console.log(err); 301 $.messager.progress('close'); 302 $.messager.alert('導出失敗', err.statusText, 'error'); 303 }); 304 305 } 306 //彈出明細信息 307 function showdetailswindow(id, index) { 308 const company = $dg.datagrid('getRows')[index]; 309 opencompanydetailwindow(company, 'Modified'); 310 } 311 function reload() { 312 $dg.datagrid('uncheckAll'); 313 $dg.datagrid('reload'); 314 } 315 //新增記錄 316 function append() { 317 company = { 318 Address: '-', 319 RegisterDate: moment().format('YYYY-MM-DD HH:mm:ss'), 320 }; 321 if (!EDITINLINE) { 322 //彈出新增窗口 323 opencompanydetailwindow(company, 'Added'); 324 } else { 325 if (endEditing()) { 326 //對必填字段進行默認值初始化 327 $dg.datagrid('insertRow', 328 { 329 index: 0, 330 row: company 331 }); 332 editIndex = 0; 333 $dg.datagrid('selectRow', editIndex) 334 .datagrid('beginEdit', editIndex); 335 hook = true; 336 } 337 } 338 } 339 //刪除編輯的行 340 function removeit() { 341 if (this.$dg.datagrid('getChecked').length <= 0 && EDITINLINE) { 342 if (editIndex !== undefined) { 343 const delindex = editIndex; 344 $dg.datagrid('cancelEdit', delindex) 345 .datagrid('deleteRow', delindex); 346 hook = true; 347 } else { 348 const rows =$dg.datagrid('getChecked'); 349 rows.slice().reverse().forEach(row => { 350 const rowindex =$dg.datagrid('getRowIndex', row); 351 $dg.datagrid('deleteRow', rowindex); 352 hook = true; 353 }); 354 } 355 } else { 356 deletechecked(); 357 } 358 } 359 //刪除該行 360 function deleteRow(id) { 361 $.messager.confirm('確認', '你肯定要刪除該記錄?', result => { 362 if (result) { 363 dodeletechecked([id]); 364 } 365 }) 366 } 367 //刪除選中的行 368 function deletechecked() { 369 const id =$dg.datagrid('getChecked').filter(item => item.Id != null && item.Id > 0).map(item => { 370 return item.Id; 371 }); 372 if (id.length > 0) { 373 $.messager.confirm('確認', `你肯定要刪除這 <span class='badge badge-icon position-relative'>${id.length} </span> 行記錄?`, result => { 374 if (result) { 375 dodeletechecked(id); 376 } 377 }); 378 } else { 379 $.messager.alert('提示', '請先選擇要刪除的記錄!', 'question'); 380 } 381 } 382 //執行刪除 383 function dodeletechecked(id) { 384 $.post('/Companies/DeleteChecked', { id: id }) 385 .done(response => { 386 if (response.success) { 387 toastr.error(`成功刪除[${id.length}]行記錄`); 388 reload(); 389 } else { 390 $.messager.alert('錯誤', response.err, 'error'); 391 } 392 }) 393 .fail((jqXHR, textStatus, errorThrown) => { 394 $.messager.alert('異常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error'); 395 }); 396 } 397 //開啓編輯狀態 398 function onClickCell(index, field) { 399 400 company = $dg.datagrid('getRows')[index]; 401 const _actions = ['action', 'ck']; 402 if (!EDITINLINE || $.inArray(field, _actions) >= 0) { 403 return; 404 } 405 406 if (editIndex !== index) { 407 if (endEditing()) { 408 $dg.datagrid('selectRow', index) 409 .datagrid('beginEdit', index); 410 hook = true; 411 editIndex = index; 412 const ed = $dg.datagrid('getEditor', { index: index, field: field }); 413 if (ed) { 414 ($(ed.target).data('textbox') ? $(ed.target).textbox('textbox') : $(ed.target)).focus(); 415 } 416 } else { 417 $dg.datagrid('selectRow', editIndex); 418 } 419 } 420 } 421 //關閉編輯狀態 422 function endEditing() { 423 424 if (editIndex === undefined) { 425 return true; 426 } 427 if (this.$dg.datagrid('validateRow', editIndex)) { 428 $dg.datagrid('endEdit', editIndex); 429 return true; 430 } else { 431 const invalidinput = $('input.validatebox-invalid', $dg.datagrid('getPanel')); 432 const fieldnames = invalidinput.map((index, item) => { 433 return $(item).attr('placeholder') || $(item).attr('id'); 434 }); 435 $.messager.alert('提示', `${Array.from(fieldnames)} 輸入有誤.`, 'error'); 436 return false; 437 } 438 } 439 //提交保存後臺數據庫 440 function acceptChanges() { 441 if (endEditing()) { 442 if ($dg.datagrid('getChanges').length > 0) { 443 const inserted = $dg.datagrid('getChanges', 'inserted').map(item => { 444 item.TrackingState = 1; 445 return item; 446 }); 447 const updated = $dg.datagrid('getChanges', 'updated').map(item => { 448 item.TrackingState = 2 449 return item; 450 }); 451 const deleted = $dg.datagrid('getChanges', 'deleted').map(item => { 452 item.TrackingState = 3 453 return item; 454 }); 455 //過濾已刪除的重複項 456 const changed = inserted.concat(updated.filter(item => { 457 return !deleted.includes(item); 458 })).concat(deleted); 459 //$.messager.progress({ title: '請等待', msg: '正在保存數據...', interval: 200 }); 460 $.post('/Companies/AcceptChanges', { companies: changed }) 461 .done(response => { 462 //$.messager.progress('close'); 463 //console.log(response); 464 if (response.success) { 465 toastr.success('保存成功'); 466 $dg.datagrid('acceptChanges'); 467 reload(); 468 hook = false; 469 } else { 470 $.messager.alert('錯誤', response.err, 'error'); 471 } 472 }) 473 .fail((jqXHR, textStatus, errorThrown) => { 474 //$.messager.progress('close'); 475 $.messager.alert('異常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error'); 476 }); 477 } 478 } 479 } 480 function rejectChanges() { 481 $dg.datagrid('rejectChanges'); 482 editIndex = undefined; 483 hook = false; 484 } 485 $(document).ready(function () { 486 //定義datagrid結構 487 $dg.datagrid({ 488 rownumbers: true, 489 checkOnSelect: false, 490 selectOnCheck: false, 491 idField: 'Id', 492 sortName: 'Id', 493 sortOrder: 'desc', 494 remoteFilter: true, 495 singleSelect: true, 496 method: 'get', 497 onClickCell: onClickCell, 498 clientPaging: false, 499 pagination: true, 500 striped: true, 501 filterRules: [], 502 onHeaderContextMenu: function (e, field) { 503 e.preventDefault(); 504 $(this).datagrid('columnMenu').menu('show', { 505 left: e.pageX, 506 top: e.pageY 507 }); 508 }, 509 onBeforeLoad: function () { 510 const that = $(this); 511 document.addEventListener('panel.onfullscreen', () => { 512 setTimeout(() => { 513 that.datagrid('resize'); 514 }, 200) 515 }) 516 }, 517 onLoadSuccess: function (data) { 518 editIndex = undefined; 519 $("button[name*='deletebutton']").prop('disabled', true); 520 $("button[name*='savebutton']").prop('disabled', true); 521 $("button[name*='cancelbutton']").prop('disabled', true); 522 }, 523 onCheck: function () { 524 $("button[name*='deletebutton']").prop('disabled', false); 525 }, 526 onUncheck: function () { 527 const checked = $(this).datagrid('getChecked').length > 0; 528 $("button[name*='deletebutton']").prop('disabled', !checked); 529 }, 530 onSelect: function (index, row) { 531 company = row; 532 }, 533 onBeginEdit: function (index, row) { 534 //const editors = $(this).datagrid('getEditors', index); 535 536 }, 537 onEndEdit: function (index, row) { 538 editIndex = undefined; 539 }, 540 onBeforeEdit: function (index, row) { 541 editIndex = index; 542 row.editing = true; 543 $("button[name*='deletebutton']").prop('disabled', false); 544 $("button[name*='cancelbutton']").prop('disabled', false); 545 $("button[name*='savebutton']").prop('disabled', false); 546 $(this).datagrid('refreshRow', index); 547 }, 548 onAfterEdit: function (index, row) { 549 row.editing = false; 550 editIndex = undefined; 551 $(this).datagrid('refreshRow', index); 552 }, 553 onCancelEdit: function (index, row) { 554 row.editing = false; 555 editIndex = undefined; 556 $("button[name*='deletebutton']").prop('disabled', true); 557 $("button[name*='savebutton']").prop('disabled', true); 558 $("button[name*='cancelbutton']").prop('disabled', true); 559 $(this).datagrid('refreshRow', index); 560 }, 561 frozenColumns: [[ 562 /*開啓CheckBox選擇功能*/ 563 { field: 'ck', checkbox: true }, 564 { 565 field: 'action', 566 title: '操做', 567 width: 85, 568 sortable: false, 569 resizable: true, 570 formatter: function showdetailsformatter(value, row, index) { 571 if (!row.editing) { 572 return `<div class="btn-group">\ 573 <button onclick="showdetailswindow('${row.Id}', ${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="查看明細" ><i class="fal fa-edit"></i> </button>\ 574 <button onclick="deleteRow('${row.Id}',${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="刪除記錄" ><i class="fal fa-times"></i> </button>\ 575 </div>`; 576 } else { 577 return `<button class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" disabled title="查看明細" ><i class="fal fa-edit"></i> </button>`; 578 } 579 } 580 } 581 ]], 582 columns: [[ 583 584 { /*名稱*/ 585 field: 'Name', 586 title: '<span class="required">@Html.DisplayNameFor(model => model.Name)</span>', 587 width: 200, 588 hidden: false, 589 editor: { 590 type: 'textbox', 591 options: { prompt: '@Html.DescriptionFor(model => model.Name)', required: true, validType: 'length[0,50]' } 592 }, 593 sortable: true, 594 resizable: true 595 }, 596 { /*組織代碼*/ 597 field: 'Code', 598 title: '<span class="required">@Html.DisplayNameFor(model => model.Code)</span>', 599 width: 120, 600 hidden: false, 601 editor: { 602 type: 'textbox', 603 options: { prompt: '@Html.DescriptionFor(model => model.Code)', required: true, validType: 'length[0,12]' } 604 }, 605 sortable: true, 606 resizable: true 607 }, 608 { /*地址*/ 609 field: 'Address', 610 title: '@Html.DisplayNameFor(model => model.Address)', 611 width: 200, 612 hidden: false, 613 editor: { 614 type: 'textbox', 615 options: { prompt: '@Html.DescriptionFor(model => model.Address)', required: false, validType: 'length[0,50]' } 616 }, 617 sortable: true, 618 resizable: true 619 }, 620 { /*聯繫人*/ 621 field: 'Contect', 622 title: '@Html.DisplayNameFor(model => model.Contect)', 623 width: 120, 624 hidden: false, 625 editor: { 626 type: 'textbox', 627 options: { prompt: '@Html.DescriptionFor(model => model.Contect)', required: false, validType: 'length[0,12]' } 628 }, 629 sortable: true, 630 resizable: true 631 }, 632 { /*聯繫電話*/ 633 field: 'PhoneNumber', 634 title: '@Html.DisplayNameFor(model => model.PhoneNumber)', 635 width: 120, 636 hidden: false, 637 editor: { 638 type: 'textbox', 639 options: { prompt: '@Html.DescriptionFor(model => model.PhoneNumber)', required: false, validType: 'length[0,20]' } 640 }, 641 sortable: true, 642 resizable: true 643 }, 644 { /*註冊日期*/ 645 field: 'RegisterDate', 646 title: '<span class="required">@Html.DisplayNameFor(model => model.RegisterDate)</span>', 647 width: 140, 648 align: 'right', 649 hidden: false, 650 editor: { 651 type: 'datebox', 652 options: { prompt: '@Html.DescriptionFor(model => model.RegisterDate)', required: true } 653 }, 654 formatter: dateformatter, 655 sortable: true, 656 resizable: true 657 }, 658 ]] 659 }).datagrid('columnMoving') 660 .datagrid('resetColumns') 661 .datagrid('enableFilter', [ 662 { /*Id*/ 663 field: 'Id', 664 type: 'numberbox', 665 op: ['equal', 'notequal', 'less', 'lessorequal', 'greater', 'greaterorequal'] 666 }, 667 { /*註冊日期*/ 668 field: 'RegisterDate', 669 type: 'dateRange', 670 options: { 671 onChange: value => { 672 $dg.datagrid('addFilterRule', { 673 field: 'RegisterDate', 674 op: 'between', 675 value: value 676 }); 677 678 $dg.datagrid('doFilter'); 679 } 680 } 681 }, 682 ]) 683 .datagrid('load', '/Companies/GetData'); 684 } 685 ); 686 687 </script> 688 <script type="text/javascript"> 689 //判斷新增編輯狀態 690 var MODELSTATE = 'Added'; 691 var companyid = null; 692 function opencompanydetailwindow(data, state) { 693 MODELSTATE = state; 694 initcompanydetailview(); 695 companyid = (data.Id || 0); 696 $("#companydetailwindow").window("open"); 697 $('#company_form').form('reset'); 698 $('#company_form').form('load', data); 699 } 700 //刪除當前記錄 701 function deletecompanyitem() { 702 $.messager.confirm('確認', '你肯定要刪除該記錄?', result => { 703 if (result) { 704 const url = `/Companies/Delete/${companyid}`; 705 $.get(url).done(res => { 706 if (res.success) { 707 toastr.success("刪除成功"); 708 $("#companydetailwindow").window("close"); 709 reload(); 710 } else { 711 $.messager.alert("錯誤", res.err, "error"); 712 } 713 }); 714 } 715 }); 716 } 717 //async 保存數據 718 async function savecompanyitem() { 719 const $companyform = $('#company_form'); 720 if ($companyform.form('enableValidation').form('validate')) { 721 let company = $companyform.serializeJSON(); 722 let url = '/Companies/Edit'; 723 //判斷是新增或是修改方法 724 if (MODELSTATE === 'Added') { 725 url = '/Companies/Create'; 726 } 727 var token = $('input[name="__RequestVerificationToken"]', $companyform).val(); 728 //$.messager.progress({ title: '請等待', msg: '正在保存數據...', interval: 200 }); 729 $.ajax({ 730 type: "POST", 731 url: url, 732 data: { 733 __RequestVerificationToken: token, 734 company: company 735 }, 736 dataType: 'json', 737 contentType: 'application/x-www-form-urlencoded; charset=utf-8' 738 }) 739 .done(response => { 740 //$.messager.progress('close'); 741 if (response.success) { 742 hook = false; 743 $companyform.form('disableValidation'); 744 $dg.datagrid('reload'); 745 $('#companydetailwindow').window("close"); 746 toastr.success("保存成功"); 747 } else { 748 $.messager.alert("錯誤", response.err, "error"); 749 } 750 }) 751 .fail((jqXHR, textStatus, errorThrown) => { 752 //$.messager.progress('close'); 753 $.messager.alert('異常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error'); 754 }); 755 } 756 } 757 //關閉窗口 758 function closecompanydetailwindow() { 759 $('#companydetailwindow').window('close'); 760 } 761 762 //判斷是否有沒有保存的記錄 763 function companyhasmodified() { 764 return hook; 765 } 766 767 768 function initcompanydetailview() { 769 //判斷是否顯示功能按鈕 770 if (MODELSTATE === 'Added') { 771 $('#deleteitem-btn-group').hide(); 772 } else { 773 $('#deleteitem-btn-group').show(); 774 } 775 776 //回車光標移動到下個輸入控件 777 //日期類型 註冊日期 778 $('#RegisterDate').datebox('textbox').bind('keydown', function (e) { 779 if (e.keyCode == 13) { 780 $(e.target).emulateTab(); 781 } 782 }); 783 } 784 </script> 785 }
上面View層的代碼很是的複雜,但都是固定格式,能夠用scaffold快速生成
打開 startup.cs 在 public void ConfigureServices(IServiceCollection services) 註冊服務 services.AddScoped<IRepositoryX, RepositoryX>();
services.AddScoped<ICustomerService, CustomerService>();
EF Core Code-First 同步更新數據庫
在 Visual Studio.Net
Package Manager Controle 運行
PM>:add-migration create_Company
PM>:update-database
PM>:更新完成
CAP 分佈式事務的解決方案及應用場景
nuget 安裝組件
PM> Install-Package DotNetCore.CAP
PM> Install-Package DotNetCore.CAP.RabbitMQ
PM> Install-Package DotNetCore.CAP.SqlServer \
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddCap(x => 4 { 5 x.UseEntityFramework<SmartDbContext>(); 6 x.UseRabbitMQ("127.0.0.1"); 7 x.UseDashboard(); 8 x.FailedRetryCount = 5; 9 x.FailedThresholdCallback = failed => 10 { 11 var logger = failed.ServiceProvider.GetService<ILogger<Startup>>(); 12 logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times, 13 requiring manual troubleshooting. Message name: {failed.Message.GetName()}"); 14 }; 15 }); 16 }