ABP 开发手记,通过做一个分类管理完整实现前后端代码
ABP 开发手记(Begin 2018-7-25)
7.25开始,启用5.4版本asp net zero,做一个最简单的分类管理和上传图片这两个功能,看从学习到完成需要多久的时间
因为工作太忙,零星抽时间弄了一下,最后做完这个功能,居然已经是9.25,整整两个月。
=============================================================================================
按照官方教程建好项目后,假定项目名称为Relyto.CoreERP
文章内容我按照正常的步骤完整做完一个mvc页面。初用这个框架的时候,由于我没有完整的阅读官方文档,拿着就开整 ,对整体架构不熟悉,做每一个步骤需要在不同的项目文件夹切换来切换去,经常找不到需要添加的内容在哪里。所以这个文档我描述了在做的过程中,每一步在哪个项目或者文件进行操作,大家在做的时候注意后面的项目后缀,对应原先的项目结构。
#2018-7-25
#1、建实体
--Namespace
Relyto.CoreERP.Core项目下,新建文件夹Channel,然后建实体
需要添加以下命名空间:
using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
更改类对应的表名,是在class类前加上以下注解:
[Table("cmn_channelinfo")]
示例:
[Table("cmn_channelinfo")]
public class ChannelInfo:FullAuditedEntity ,IMustHaveTenant
{}
对于多租户,实现 IMustHaveTenant,并添加以下语句
public int TenantId { get; set; }
#2、添加到DbContext
Relyto.CoreERP.EntityFrameworkCore项目下\EntityFrameworkCore文件夹
修改:AbpZeroTemplateDbContext.cs
Add Namespace:using Relyto.CoreERP.Channel;
在下面添加一行代码,用于下面命令行把这张表结构生成到数据库中去:
public virtual DbSet<ChannelInfo> ChannelInfos { get; set; }
在程序包管理器控制台,先选择项目为Relyto.CoreERP.EntityFrameworkCore,然后在下面执行以下命令:
Add-Migration "Add Channel Info"
Update-Database
,然后检查一下数据库,请应该是生成了
#3.APPlication 层
Relyto.CoreERP.Application项目下新建ChannelInfo文件夹
3.1 IChannelInfoAppService
using Abp.Application.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Relyto.CoreERP.Channel;
using Relyto.CoreERP.Channel.Dtos;
using Abp.Application.Services.Dto;
namespace Relyto.CoreERP.Channel
{
public interface IChannelInfoAppService: IApplicationService
{
Task<ListResultDto<ChannelInfoListDto>> GetAll(GetAllChannelInfoInput input);
System.Threading.Tasks.Task Create(CreateChannelInfoInput input);
}
}
建立Dtos文件夹,生成Get,Create的方法的参数类
3.2 ChannelInfoListDto
using Abp.Application.Services.Dto;
using Abp.AutoMapper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Relyto.CoreERP.Channel;
using Relyto.CoreERP.SharedEnum;
namespace Relyto.CoreERP.Channel.Dtos
{
[AutoMapFrom(typeof(ChannelInfo))]
public class ChannelInfoListDto:FullAuditedEntityDto
{
public string Title { get; set; }
public string Description { get; set; }
public string SubTitle { get; set; }
public string ChannelCode { get; set; }
public string EnglishName { get; set; }
public string ImageUrl { get; set; }
public short ChannelIndex { get; set; }
public int ParentID { get; set; }
public EnumState State { get; set; }
public bool IsLastNode { get; set; }
}
}
3.3 GetAllChannelInfoInput
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Relyto.CoreERP.Channel.Dtos
{
public class GetAllChannelInfoInput
{
public int ParentID { get; set; }
}
}
3.4 CreateChannelInfoInput
using Abp.AutoMapper;
using Relyto.CoreERP.SharedEnum;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Relyto.CoreERP.Channel.Dtos
{
[AutoMapTo(typeof(ChannelInfo))]
public class CreateChannelInfoInput
{
public const int MaxTitleLength = 20;
public const int MaxDescriptionLength = 100; //64KB
/// <summary>
/// 标题
/// </summary>
[Required]
[MaxLength(MaxTitleLength)]
public string Title { get; set; }
/// <summary>
/// 描述
/// </summary>
[MaxLength(MaxDescriptionLength)]
public string Description { get; set; }
/// <summary>
/// 子标题
/// </summary>
[MaxLength(MaxTitleLength)]
public string SubTitle { get; set; }
/// <summary>
/// channel Code附加
/// </summary>
[MaxLength(MaxTitleLength)]
public string ChannelCode { get; set; }
/// <summary>
/// 英文名
/// </summary>
[MaxLength(MaxTitleLength)]
public string EnglishName { get; set; }
/// <summary>
/// channel 附加的图片地址
/// </summary>
public string ImageUrl { get; set; }
/// <summary>
/// 显示顺序
/// </summary>
public short ChannelIndex { get; set; }
/// <summary>
/// 上级ID
/// </summary>
[Required]
public int ParentID { get; set; }
/// <summary>
/// 状态
/// </summary>
public EnumState State { get; set; }
/// <summary>
/// 是否末级
/// </summary>
public bool IsLastNode { get; set; }
}
}
#4.实现IChannelInfoAppService
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Relyto.CoreERP;
using Relyto.CoreERP.Channel;
using Relyto.CoreERP.Channel.Dtos;
using Abp.Application.Services.Dto;
using Microsoft.EntityFrameworkCore;
namespace Relyto.CoreERP.Channel
{
public class ChannelInfoAppService:AbpZeroTemplateAppServiceBase,IChannelInfoAppService
{
private readonly IRepository<ChannelInfo> _channelInfoRepository;
public ChannelInfoAppService(IRepository<ChannelInfo> channelRepository)
{
_channelInfoRepository = channelRepository;
}
public async System.Threading.Tasks.Task Create(CreateChannelInfoInput input)
{
var channelInfo = ObjectMapper.Map<ChannelInfo>(input);
await _channelInfoRepository.InsertAsync(channelInfo);
}
public async Task<ListResultDto<ChannelInfoListDto>> GetAll(GetAllChannelInfoInput input)
{
var channelInfos = await _channelInfoRepository
.GetAll()
.Where(m=>m.ParentID==input.ParentID)
.OrderByDescending(t => t.CreationTime)
.ToListAsync();
return new ListResultDto<ChannelInfoListDto>(
ObjectMapper.Map<List<ChannelInfoListDto>>(channelInfos)
);
}
}
}
#5.添加测试
跳过了
#6.Adding a New Menu Item,添加新菜单
找到
Relyto.CoreERP.Web.Mvc项目下AppAreaName\Startup\找到AppAreaNameNavigationProvider
类似这样:
.AddItem(new MenuItemDefinition(
AppAreaNamePageNames.Common.DemoUiComponents,
L("图片上传"),
url: "AppAreaName/ImageManager",
icon: "flaticon-shapes"
)
);
或者需要权限:
.AddItem(new MenuItemDefinition(
AppAreaNamePageNames.Common.DemoUiComponents,
L("DemoUiComponents"),
url: "AppAreaName/DemoUiComponents",
icon: "flaticon-shapes",
requiredPermissionName: AppPermissions.Pages_DemoUiComponents
)
)
6.1 AppAreaNamePageNames 添加几个常量,用于菜单等需要的时候使用
public static class Channel
{
public const string NewChannel = "Channel.New";
public const string ViewAll = "Channel.ViewAll";
public const string EditChannel = "Channel.Edit";
}
6.2添加权限名称,这些名称在后面对应的JS中也要用到
Relyto.CoreERP.Authorization.AppPermissions
public const string Pages_Administration_ChannelManager = "Pages.Administration.Pages_Administration_ChannelManager";
6.3在Relyto.CoreERP.Core项目Localization\AbpZeroTemplate下添加语言词条,这个地方要记到,以后要添加词条资源都在这里添加
比如在代码里使用到 L("ChannelManager"),需要在对应的zh文件里面添加:
<text name="ChannelManager">分类管理</text>
6.3 创建权限点
Relyto.CoreERP.Core\Authorization\AppAuthorizationProvider下
administration.CreateChildPermission(AppPermissions.Pages_Administration_ChannelManager, L("ChannelManager"));
#7.创建MVC
Relyto.CoreERP.Web.Mvc\Areas\AppAreaName\Controllers\ChannelInfoController
创建control.直接添加control.Relyto.CoreERP.Web.Mvc\Areas\AppAreaName\Controllers 继承的是 AbpZeroTemplateControllerBase
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Abp.AspNetCore.Mvc.Authorization;
using Abp.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Relyto.CoreERP.Authorization;
using Relyto.CoreERP.Web.Controllers;
namespace Relyto.CoreERP.Web.Mvc.Areas.AppAreaName.Controllers
{
[Area("AppAreaName")]
[AbpMvcAuthorize(AppPermissions.Pages_Administration_ChannelManager)]
public class ChannelInfoController : AbpZeroTemplateControllerBase
#8.复杂的客户端脚本,以树形为例,分类信息
在\Relyto.CoreERP.Web.Mvc\Areas\AppAreaName\Views\ChannelInfo
而对应的脚本信息,css信息,确放在:
Relyto.CoreERP.Web.Mvc\wwwroot\view-resources\Areas\AppAreaName\Views\ChannelInfo
目录,使用时注意对照
另外,在View页面里,涉及的字符串,我们一般直接写,但是在这里一般用L("Key")的方式,要对照
Relyto.CoreERP.Core\Localization\AbpZeroTemplate
里面的模板文件添加
前期比如我只添加中文:AbpZeroTemplate-zh-CN.xml里对照添加
举例:
<div id="ChannelInfoEmptyInfo" ng-if="!vm.organizationTree.unitCount" class="text-muted">
@L("ChannelNoInfoYet")
</div>
大量的这种。是很不习惯的
研究JS,还没明白tree是怎么生成的,噢。原来是用的JStree,以前没有用过,难怪不懂,NND前端不懂的东西太多了,一入前端深似海。
学习地方:https://www.jstree.com/
JS Tree: Create an instance
Once the DOM is ready you can start creating jstree instances.
$(function () { $('#jstree_demo_div').jstree(); });
--------------------------------------------------------------------
读代码 :在 里面给了一个div ChannelInfoEditTree
在js里面,调用了
channelinfoTree.init();
这个方法代码:
init: function () {
channelinfoTree.getTreeDataFromServer(function (treeData) {
channelinfoTree.setUnitCount(treeData.length);
channelinfoTree.$tree
.on('changed.jstree', function (e, data) {
if (data.selected.length != 1) {
channelinfoTree.selectedOu.set(null);
} else {
var selectedNode = data.instance.get_node(data.selected[0]);
channelinfoTree.selectedOu.set(selectedNode);
}
})
.on('move_node.jstree', function (e, data) {
var parentNodeName = (!data.parent || data.parent == '#')
? app.localize('Root')
: channelinfoTree.$tree.jstree('get_node', data.parent).original.displayName;
abp.message.confirm(
app.localize('OrganizationUnitMoveConfirmMessage', data.node.original.displayName, parentNodeName),
function (isConfirmed) {
if (isConfirmed) {
_channelinfoService.moveOrganizationUnit({
id: data.node.id,
newParentId: data.parent
}).done(function () {
abp.notify.success(app.localize('SuccessfullyMoved'));
channelinfoTree.reload();
}).fail(function (err) {
channelinfoTree.$tree.jstree('refresh'); //rollback
setTimeout(function () { abp.message.error(err.message); }, 500);
});
} else {
channelinfoTree.$tree.jstree('refresh'); //rollback
}
}
);
})
.jstree({
'core': {
data: treeData,
multiple: false,
check_callback: function (operation, node, node_parent, node_position, more) {
return true;
}
},
types: {
"default": {
"icon": "fa fa-folder m--font-warning"
},
"file": {
"icon": "fa fa-file m--font-warning"
}
},
contextmenu: {
items: channelinfoTree.contextMenu
},
sort: function (node1, node2) {
if (this.get_node(node2).original.displayName < this.get_node(node1).original.displayName) {
return 1;
}
return -1;
},
plugins: [
'types',
'contextmenu',
'wholerow',
'sort',
'dnd'
]
});
#9.改好了客户端展示页面,要实现功能,先从createmodal开始吧
9.1 在Relyto.CoreERP.Web.Mvc\Areas\AppAreaName\Models\Channel
新建了CreateChannelInfoModalViewModel类,里面就实现了个parentid,现在还不知道怎么用
9.2更改View,添加了Relyto.CoreERP.Web.Mvc\Areas\AppAreaName\Views\ChannelInfo\_CreateModal.cshtml
里面好像头部和尾部都是现成的。
遇的到,半天不能弹出modal框,原来是JS路径写错了,下次记得检查这个:
一开始我没有在控制文件创建creeateModal这个方法,所以直接报错。
控制文件创建了以后,因为js不对一直没打开
var _createModal = new app.ModalManager({
viewUrl: abp.appPath + 'AppAreaName/ChannelInfo/CreateModal',
scriptUrl: abp.appPath + 'view-resources/Areas/AppAreaName/Views/ChannelInfo/_CreateModal.js', =》刚才就是这个路径写错了,所以就不弹出圣诞框 。
modalClass:'CreateChannelInfoModal' =》这个类名,就是 _CreateModal里面开始那个类。如果对不上,则后来点保存没有反应。我找了半天才发现这个问题。
function() {
app.modals.CreateChannelInfoModalViewModel = function () {
就是上面这个代码
这个类里面就是通过var channelInfo = _$form.serializeFormToObject();
直接就把界面上的数据,form里面的,序列化,然后就传给后端,后端使用的是DtoInput,也不知道 怎么弄的,居然可以直接解析出来。
然后就去写数据库了。感觉客户端主要是太不熟悉了。慢慢看代码 还是看的懂。
套路就是
服务端App定义的服务,客户端可以直接变成js来用,这个真牛逼。当然要注意把方法的第一个字母小写了。
cshtml页面里,直接用DtoInput的字段名来做数据,自然就可以映射到服务端dto去,应该还要整理一下文本框架,单选,多选,日期,图片等各种类型的客户端处理方法。
从user那个来看,还可以有更复杂的页面方法来实现 。
这样客户端的逻辑代码就相对多一些。
});
9.3 在APPService 那个项目可以通过AbpSession.TenantId 来获取当前的TenantID.
9.4.已搞定添加到数据库,但是现在显示 还是undefind
#10.解决显示问题
1.解决了分类显示那里不能正确显示的问题,搞明白了()里面是对子节点的统计
原来是这个在搞定节点显示:
generateTextOnTree: function (ou) {
var itemClass = ou.memberCount > 0 ? ' ou-text-has-members' : ' ou-text-no-members';
return '<span title="' + ou.id + '" class="ou-text' + itemClass
+ '" data-ou-id="' + ou.id + '">'
+ app.htmlUtils.htmlEncodeText(ou.title)
+ ' (<span class="ou-text-member-count">'
+ ou.memberCount
+ '</span>) <i class="fa fa-caret-down text-muted"></i></span>';
},
其中有几个注意事项:
a.title这种字段,在服务端是全大定,到了这里是首字母小写才正常。
b.memberCount是服务端app层统计出来的,而organ那里有个方法,我还没有写过那种方法,待确定。他用了一个join+new就搞定了分类统计的问题。
#11 遇到本地在树状选择里没有复选框的问题
1.编辑频道信息,结果发现做角色与权限管理那里,树状没有复选框,不知道为什么在我的界面上没有回来
最后我在
Relyto.CoreERP.Web.Mvc\wwwroot\view-resources\Areas\AppAreaName\Views\Common\PermissionTree.js
修改了一个变量:
'checkbox': {
keep_selected_style: true, --false =>true
three_state: false,
cascade: '',
visible:true
},
可以看到蓝色背景的选择结果,勉强可以,不晓得为哈我这个上面不出来复选框。
折腾了一晚上,只解决了这个问题。效率很低啊
还有就是把权限又搞懂了一点。 现在还建了子权限菜单。在数据库里有个表,对应createpermison那块
https://blog.csdn.net/new0801/article/details/54766984这篇文章讲的比较清楚,一步一步做就可以了
#还是前端工作
发现前端工作量真大啊。写个修改,也要写那么久
没有解决到复选框的问题,现在发现abp前端这个好麻烦啊,每个controller对应的方法都要有个Model类。
现在还全部是用的实体复制,没有做变更,空了还要来整理。权限、实体内容 这一块,然后写个文档。
又是checkbox。坑,在编辑的时候,一选中chekcobx就valid error.搞了半天,要用这种style才能继续的下去,原因嘛,我也不懂,看起来怪怪的
<label for="IsLastNode" class="m-checkbox">
<input id="IsLastNode" type="checkbox" name="IsLastNode" value="true" @(Model.IsLastNode ? "checked=\"checked\"" : "")>
@L("IsLastNode")
<span></span>
</label>
我看很多地方都是这样写的,抄过来就可以用,但原因嘛,没明白 。
现在基本完成添加和修改了。接下来,那就把展示那里,把点击后,其附加信息显示出来这块做了嘛。
坑:juqery设置<input textbox 不是用text(),而是用val();
我最后又遇到checkbox,没搞定用javascript更改期check值。最后换文本了。我这个版本跟checkbox有仇?怎么都不出来chekcbox
在Service Update那里,从DBRepority取出来的实体不能用object.Automapper去修改数据。必须用传统的赋值方法修改过去。
#2018-8-6
1.查看信息做完后,考虑做添加下级,结果发现添加下级的时候,我没有把parentid在controller那里添加进去,修改后,数据到是对了
接下来就是不能分组显示。查看organunit,使用了一种不太懂的语法完成。我修改了getall()以后,还是不分组,看代码,似懂非懂
细致的观察了一下js代码,发现是把一个parentId写成了parnetID。原来写在了大写。在js里面首字母小写后,后面就不对了。
这种约定我没有找到文档,但在abp里面确实存在这种约定,可能大家都不用这种方式开发,我也没有看到相关的文档。
更奇怪的是,我做了 按channelindex排序后发现是倒序的,后来查看了app service层确实是orderbydesending。所以不怪别个。
目前看来树级的状态基本正常了,接下来就删除 了。
这玩意儿还缺一个刷新的按钮,从服务器重新取数据来刷一下。
#2018-8-9
1.终于在今晚完成了删除功能,现在除()里面显示的总数据不对,缺权限完,其他功能基本完成,跌跌撞撞用了10多天,都还没有完全搞完,前端真是坑啊。
还有图片上传的功能还没有做呢。
2.JS端的权限搞懂了
var _permissions = {
create: abp.auth.isGranted('Pages.Administration.ChannelManager.Create'),
edit: abp.auth.isGranted('Pages.Administration.ChannelManager.Edit'),
delete: abp.auth.isGranted('Pages.Administration.ChannelManager.Delete')
};
这个就是创建权限点的,对应D:\Project\2018\Relyto.CoreERP\aspnet-core\src\Relyto.CoreERP.Core\Authorization\AppAuthorizationProvider.cs
下的权限点,注意里面的isGranted 我用了 这个,没有用原来的haspermit那个方法。另外,后面的.是字条串常量。要去常量类里copy
接下来就是controll层和Application的Service要同样加权限。
service层是: [AbpAuthorize(AppPermissions.Pages_Administration_ChannelManager_Delete)]
controll层是:[AbpMvcAuthorize(AppPermissions.Pages_Administration_ChannelManager_Edit)]
有点区别的。
到此权限暂时可以用了哈。
现在主要就是图片上传的功能 了。
顺便还添加了一个refresh方法。
#2018-8-22
1.搞清楚了图像上传后,是存了一个GUID,放在[AppBinaryObjects]中,目前只写了数据库保存,还没有写本地保存。
所以我把代码全部改了一下,关联表只存GUID,不存URL
因为是数据库,我看JS中当前图像最大存放:9990000,实测小于1.2M,没看懂这个单位。
不过折腾了一遍。完善了添加、修改删除后刷新的问题,解决了添加、删除、修改过程中图片显示、上传的问题,以及服务端在编辑、删除同步删除相关图片的问题。
#2018-9-14
1.找了个前端的上传图片的框架bootstrap-fileinput,好不容易搞定了前端,但是还没有搞定后台存储的问题。
#2018-9-17
1.发现在cshtml端,必须通过
@section Styles
{
<link rel="stylesheet" abp-href="/view-resources/Areas/AppAreaName/Views/ImageManager/bootstrap-fileinput/css/fileinput.min.css" asp-append-version="true" />
}
@section Scripts
{
<script abp-src="/view-resources/Areas/AppAreaName/Views/ImageManager/bootstrap-fileinput/js/fileinput.min.js" asp-append-version="true"></script>
<script abp-src="/view-resources/Areas/AppAreaName/Views/ImageManager/bootstrap-fileinput/js/locales/zh.js" asp-append-version="true"></script>
<script abp-src="/view-resources/Areas/AppAreaName/Views/ImageManager/upload_fileinput.js" asp-append-version="true"></script>
}
这样的方式引入样式和js文件,才能正确解决跨域的问题。也解决了文件上传不发送到controller端的问题
2.在文件保存时,如果发现文件路径所在的文件夹没有创建,会报错,比如uploadfile文件夹没有创建,会报内部错误。