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 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> 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

        ///

        /// 标题

        ///

        [Required]

        [MaxLength(MaxTitleLength)]

        public string Title { get; set; }

        ///

        /// 描述

        ///

        [MaxLength(MaxDescriptionLength)]

        public string Description { get; set; }

        ///

        /// 子标题

        ///

        [MaxLength(MaxTitleLength)]

        public string SubTitle { get; set; }

        ///

        /// channel Code附加

        ///

        [MaxLength(MaxTitleLength)]

        public string ChannelCode { get; set; }

        ///

        /// 英文名

        ///

        [MaxLength(MaxTitleLength)]

        public string EnglishName { get; set; }

        ///

        /// channel 附加的图片地址

        ///

        public string ImageUrl { get; set; }

        ///

        /// 显示顺序

        ///

        public short ChannelIndex { get; set; }

        ///

        /// 上级ID

        ///

        [Required]

        public int ParentID { get; set; }

        ///

        /// 状态

        ///

        public EnumState State { get; set; }

        ///

        /// 是否末级

        ///

        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 _channelInfoRepository;

        public ChannelInfoAppService(IRepository channelRepository)

        {

            _channelInfoRepository = channelRepository;

        }

        public async System.Threading.Tasks.Task Create(CreateChannelInfoInput input)

        {

            var channelInfo = ObjectMapper.Map(input);

            await _channelInfoRepository.InsertAsync(channelInfo);

        }

        public async Task> GetAll(GetAllChannelInfoInput input)

        {

            var channelInfos = await _channelInfoRepository

                .GetAll()               

                .Where(m=>m.ParentID==input.ParentID)

                .OrderByDescending(t => t.CreationTime)

                .ToListAsync();

            return new ListResultDto(

                ObjectMapper.Map>(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文件里面添加:

  分类管理

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里对照添加

举例:

                        @L("ChannelNoInfoYet")

       

大量的这种。是很不习惯的

研究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 ''

                    + app.htmlUtils.htmlEncodeText(ou.title)

                    + ' ('

                    + ou.memberCount

                    + ') ';

            },

其中有几个注意事项:

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才能继续的下去,原因嘛,我也不懂,看起来怪怪的


           

            @L("IsLastNode")

           

       

我看很多地方都是这样写的,抄过来就可以用,但原因嘛,没明白 。

现在基本完成添加和修改了。接下来,那就把展示那里,把点击后,其附加信息显示出来这块做了嘛。

坑:juqery设置

我最后又遇到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

    {

   

}

@section Scripts

    {

   

   

   

}

这样的方式引入样式和js文件,才能正确解决跨域的问题。也解决了文件上传不发送到controller端的问题

2.在文件保存时,如果发现文件路径所在的文件夹没有创建,会报错,比如uploadfile文件夹没有创建,会报内部错误。

你可能感兴趣的