大师网-带你快速走向大师之路 解决你在学习过程中的疑惑,带你快速进入大师之门。节省时间,提升效率

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文件夹没有创建,会报内部错误。