wenmo8 发布的文章

在较早期的随笔《
ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用
》已经介绍了Web API调用类的封装处理,虽然这些调用类我们可以使用代码生成工具快速生成,不过自定义接口,还是需要我们对这些接口进行实现,以便发起对Web API的调用,并获得相应的数据返回。本篇随笔介绍使用API调用类的封装类,进行函数的抽象,根据方法名称的推断,构建URL或者WebClient的请求类型,从而实现所有API调用函数的简化处理。

1、ABP框架服务端和客户端的处理

ABP框架的架构图示,如下图所示(以字典模块为例说明)

针对Web API接口调用的封装,为了适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

而ABP的Web API调用类则需要对Web API接口调用进行封装,如下所示。

如对于字典模块的API封装类,它们继承一个相同的基类,然后实现特殊的自定义接口即可,这样可以减少常规的Create、Get、GetAll、Update、Delete等操作的代码,这些全部由调用基类进行处理,而只需要实现自定义的接口调用即可。

2、Web API调用类的简化处理

我们对于常规的Web API调用接口处理,如下代码所示。

        public async virtual Task<AuthenticateResult> Authenticate(string username, stringpassword)
{
var url = string.Format("{0}/api/TokenAuth/Authenticate", ServerRootAddress);var input = new{
UsernameOrEmailAddress
=username,
Password
=password
};
var result = await apiClient.PostAsync<AuthenticateResult>(url, input);returnresult;
}

这种方法的处理,就需要自己拼接URL地址,以及传递相关的参数,一般情况下,我们的Web API Caller层类的函数和Web API控制器的方法是一一对应的,因此方法名称可以通过对当前接口名称的推断进行获得,如下所示。

        public async Task<bool>ChangePassword(ChangePasswordDto input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) return await apiClient.PostAsync<bool>(url, input);
}

函数AddRequestHeaders 通过在调用前增加对应的AccessToken信息,然后URL通过当前方法的推断即可构建一个完整的URL,但是这个也仅仅是针对POST的方法,因为ABP框架根据方法的名称前缀的不同,而采用POST、GET、Delete、PUT等不同的HTTP处理操作。

如GET方法,则是需要使用GET请求

        public async Task<List<RoleDto>> GetRolesByUser(EntityDto<long>input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url =GetUrlParam(input, url);var result = await apiClient.GetAsync<List<RoleDto>>(url);returnresult;
}

而对于删除方法,则使用下面的DELETE请求,DELETE 和PUT操作,需要把参数串联成GET的URL形式,类似 url += string.Format("?Id={0}", id); 这样方式

        public virtual asyncTask Delete(TDeleteInput input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url +=GetUrlParam(input, url);var result = awaitapiClient.DeleteAsync(url);returnresult;
}

对于更新的操作,使用了PUT方法

        public async virtual Task<TEntityDto>Update(TUpdateInput input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) var result = await apiClient.PutAsync<TEntityDto>(url, input, null);returnresult;
}

上面这些方法,我们根据规律,其实可以进一步进行简化,因为这些操作大多数类似的。

首先我们看到变化的地方,就是根据方法的前缀采用GET、POST、DELETE、PUT方法,还有就是URL串联字符串的不同,对于GET、Delete方法,参数使用的是组成URL方式,参数使用的是JSON提交内容方式。

根据这些变化,我们在基类提炼一个统一的处理方法DoActionAsync 来处理这些不同的操作。

        /// <summary>
        ///根据方法名称自动执行GET/POST/PUT/DELETE请求方法/// </summary>
        /// <param name="method"></param>
        /// <param name="input"></param>
        protected virtual async Task DoActionAsync(MethodBase method, object input = null)
{
await DoActionAsync<object>(method, input);
}
        /// <summary>
        ///根据方法名称自动执行GET/POST/PUT/DELETE请求方法/// </summary>
        /// <param name="method"></param>
        /// <param name="input"></param>
        protected virtual async Task<TResult> DoActionAsync<TResult>(MethodBase method, object input = null)
{
AddRequestHeaders();
//加入认证的token头信息 string action =GetMethodName(method);var url = string.Format("{0}/api/services/app/{1}/{2}", ServerRootAddress, DomainName, action);//获取访问API的地址(未包含参数) var httpVerb =DynamicApiVerbHelper.GetConventionalVerbForMethodName(action);if(httpVerb == HttpVerb.Get || httpVerb ==HttpVerb.Delete)
{
if (input != null)
{
//Get和Delete的操作,需要组装URL参数 url =GetUrlParam(input, url);
}
}
int? timeout = null;return await apiClient.DoActionAsync<TResult>(url, timeout, httpVerb.ToString().ToLower(), input);
}

这样,有了这两个函数的支持,我们可以简化很多操作代码了。

例如对于Update方法,简化的代码如下所示。

        public async virtual Task<TEntityDto>Update(TUpdateInput input)
{
return await DoActionAsync<TEntityDto>(MethodBase.GetCurrentMethod(), input);
}

对于删除操作,简化的代码依旧也是一行代码

        public virtual asyncTask Delete(TDeleteInput input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}

GET操作,也是一行代码

        public async virtual Task<TEntityDto>Get(TGetInput input)
{
return await DoActionAsync<TEntityDto>(MethodBase.GetCurrentMethod(), input);
}

现在你看到,所有的客户端API封装类调用,都已经非常简化,大同小异了,主要就是交给基类函数进行推断调用处理即可。

如用户操作的APICaller类的代码如下所示。

这样我们再多的接口,都一行代码调用解决问题,非常简单,从此客户端封装类的实现就非常简单了,只需要注意有没有返回值即可,其他的都没有什么不同。

只需要注意的是,我们定义接口的时候,尽可能使用复杂类型对象,这样就可以根据对象属性名称和值进行构建URL或者JSON的了。

在前面随笔《
ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理
》中介绍了基于ABP框架服务构建的Winform客户端,客户端通过Web API调用的方式进行获取数据,从而实现了对组织机构、角色、用户、权限等管理,其中没有涉及菜单部分,本篇随笔介绍在ABP框架中实现菜单的管理,菜单是作为Winform或者Web动态构建界面的一个重要元素,同时也是作为角色权限控制的部分资源。

1、菜单的列表展示和管理

一般情况下,菜单的树形列表的显示可以分为多个节点,节点可以收缩也可以展开,当然节点是有不同的图标的了。这样就可以把很多功能点整合在一个树列表里面了,树的节点也可以分为很多级别,很多层次


如果我们想按照业务的范畴来区分,也可以分为多个模块展示,类似选项卡的方式,一个模块的功能菜单列表集合在一起展示,如下所示。

上面是我Winform开发框架和混合式开发框架的Winform界面中呈现菜单的界面,对于ABP开发框架来说,我们也只是获取数据方式不同,业务范畴的管理也没有什么不一样,我们依旧可以在服务器端配置好系统的菜单记录,然后基于ABP的Winform界面,同样管理这些内容即可。

下面是ABP框架中对于菜单资源管理的列表界面。

左边我们通过TreeList列表进行展示,右侧通过分页控件列表的方式进行展示,还是比较标准的Winform界面展示。

编辑或者创建菜单的界面如下所示。

菜单对于角色来说,应该是一种界面资源,可以通过配置进行管理对应角色用户的菜单。

2、菜单模块的实现逻辑

为了开发菜单模块,我们需要先定义好菜单的存储数据表,定义菜单表和角色菜单的中间关系表如下所示。

这个菜单模块定位为Web和Winform都通用的,因此菜单表中增加多了一些字段信息。

在数据库里增加这两个表后,就可以使用代码生成工具进行框架代码的生成和Winform界面代码的生成了。

生成框架后,对应的应用服务层类代码如下所示。

这个生成的类,默认具有基类的增删改查分页等接口方法,同时我们也会生成对应的Web API Caller层的类代码,代码如下所示。

Winform界面生成标准界面后进行布局的一定调整,左侧增加TreeList控件,设计界面如下所示。

获取列表数据的函数定义在GetData函数里面,函数代码如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<MenuDto>>GetData()
{
MenuPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new MenuPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else if(!IsNormalSearch && this.tree.FocusedNode != null)
{
//构建分页的条件和查询条件 pagerDto = new MenuPagedDto(this.winGridViewPager1.PagerInfo)
{
PID
= string.Concat(this.tree.FocusedNode.GetValue(Id_FieldName))
};
}
else{//构建分页的条件和查询条件 pagerDto = new MenuPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 Name = this.txtName.Text.Trim(),
WinformType
= this.txtWinformType.Text.Trim()
};
}
var result = awaitMenuApiCaller.Instance.GetAll(pagerDto);returnresult;
}

分页控件的数据绑定代码如下所示,这些都是根据Winform界面配置自动生成的代码。

            this.winGridViewPager1.DisplayColumns = "EmbedIcon,Name,Seq,Visible,Expand,WinformType,Tag,CreationTime";this.winGridViewPager1.ColumnNameAlias = await MenuApiCaller.Instance.GetColumnNameAlias();//字段列显示名称转义//获取分页数据列表
            var result = awaitGetData();//设置所有记录数和列表数据源
            this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的赋值,更新分页信息
            this.winGridViewPager1.DataSource = result.Items;

而TreeList列表是我们后来增加上去的,需要额外进行数据的绑定和处理,初始化树列表处理代码如下所示。

        /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();//控件扩展函数封装处理 this.tree.CreateColumn("Name", "菜单名称", 160, true);this.tree.InitTree("Id", "PID", null, false, false);//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e) =>{int maxCount = this.imageCollection1.Images.Count;var index = e.Node.Level < maxCount ? e.Node.Level : 0;
e.SelectImageIndex
=index;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}

获取列表数据并绑定树列表的数据源如下所示

        /// <summary>
        ///绑定树的数据源/// </summary>
        private asyncTask BindTree()
{
var pageDto = new MenuPagedDto();var result = awaitMenuApiCaller.Instance.GetAll(pageDto);this.tree.DataSource =result.Items;this.tree.ExpandAll();
}

而界面显示的时候,加载并显示左侧树列表数据如下代码所示。

        private async void FrmMenu_Load(objectsender, EventArgs e)
{
//列表信息 InitTree();
InitSearchControl();
awaitBindTree();
}

删除菜单的时候,我们一般想把当前菜单和下面的子菜单一并级联删除,实现这个方法,我们需要在服务端自定义实现,如下是应用服务层的实现方法。

        /// <summary>
        ///移除节点和子节点/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[UnitOfWork]public virtual async Task DeleteWithSubNode(EntityDto<string>input)
{
var children = await _repository.GetAllListAsync(ou => ou.PID ==input.Id);foreach (var child inchildren)
{
await DeleteWithSubNode(new EntityDto<string>(child.Id));//递归删除 }await_repository.DeleteAsync(input.Id);
}

我们这里显示声明了UnitOfWork标记,说明这个操作的原子性,全部成功就成功,否则失败的处理。

而客户端的Web API 封装调用类,对这个Web API接口的封装,根据上篇随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》简化后的处理代码如下所示。

3、角色菜单管理

菜单的管理整体操作和常规的业务表处理一样,没有太多特殊的地方,下面介绍一下角色包含菜单的管理操作。

前面介绍了角色包含菜单的管理界面如下所示。

界面主要是列出所有菜单,并勾选上该角色可以使用的菜单。这个角色包含的菜单和角色包含的权限处理上比较相似。

首先我们需要定义一个角色DTO对象中的菜单集合属性,如下所示。

在界面上获取勾选上的权限和菜单ID集合,存储在对应的列表里面。

        /// <summary>
        ///编辑或者保存状态下取值函数/// </summary>
        /// <param name="info"></param>
        private voidSetInfo(RoleDto info)
{
info.DisplayName
=txtDisplayName.Text;
info.Name
=txtName.Text;
info.Description
=txtDescription.Text;

info.Permissions
= GetNodeValues(this.tree, "Name");
info.Menus
= GetNodeValues(this.treeMenu, "Id");
}

在应用服务层的RoleAppService类里面,我们创建或者更新角色的时候,需要更新它的权限和菜单资源,如下代码是创建角色的函数。

        /// <summary>
        ///创建角色对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<RoleDto>Create(CreateRoleDto input)
{
CheckCreatePermission();
var role = ObjectMapper.Map<Role>(input);
role.SetNormalizedName();

CheckErrors(
await_roleManager.CreateAsync(role));await CurrentUnitOfWork.SaveChangesAsync(); //It's done to get Id of the role. awaitUpdateGrantedPermissions(role, input.Permissions);awaitUpdateGrantedMenus(role, input.Menus);returnMapToEntityDto(role);
}

同理,更新角色一样处理这两个部分的资源

        /// <summary>
        ///更新角色对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<RoleDto>Update(RoleDto input)
{
CheckUpdatePermission();
var role = await_roleManager.GetRoleByIdAsync(input.Id);

ObjectMapper.Map(input, role);

CheckErrors(
await_roleManager.UpdateAsync(role));awaitUpdateGrantedPermissions(role, input.Permissions);awaitUpdateGrantedMenus(role, input.Menus);returnMapToEntityDto(role);
}

以上就是菜单的管理,和角色包含菜单的维护操作,整个开发过程主要就是使用代码生成工具来快速生成框架各个层的代码,以及Winform界面的代码,这样在进行一定的函数扩展以及界面布局调整,就可以非常方便、高效的完整一个业务模块的开发工作了。

一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查询来使用,本篇随笔引入了另外一种配置信息的定义,实现更加简化的处理,本篇随笔着重介绍两者之间的差异和不同的地方。

1、ABP框架的配置管理

如下面是邮件配置信息,配置信息一般先继承自SettingProvider,初始化定义后,才能被系统所使用。

EmailSettingProvider
:继承自SettingProvider, 将
SMTP的各项设置封装成
SettingDefinition
,并以数组形式返回

配置的管理类,主要通过接口ISettingManager来进行统一管理的,底层协同了SettingStore配置存储和SetttingDefinitionMananger的配置定义管理两个部分。

这种方式的配置信息,糅合了配置项的定义(强制性),以及多语言特性的处理,根据不同的语言返回不同的配置名称,同时也整合了缓存信息的处理,以减少系统的一些消耗。

不过从上面的图示我们也可以看到,整个配置模块由于引入这些内容,导致处理起来必须按部就班的创建配置管理类,定义配置信息,重新编译系统后,然后才能进行信息的调用,因此这些配置信息必须预定义。而且管理起来协同这些类的处理,也略显得有点复杂化。

在ABP核心模块的启动过程中,会预先初始化这些配置管理类,如下代码所示

然后在AddSettingProviders中加入预先定义好的配置类。

接着在完成初始化过程中,有配置定义类统一根据这些配置对象,进行定义的初始化,这样才能在系统中进行使用。

配置定义的管理类接口,可以用下面这个图示进行说明。

以上就是在ABP框架中,基于配置模块的管理过程。

一般情况下,如果我们需要在Web API端中对这些接口进行调用管理,如对用户或者系统Email配置信息的获取和修改,那么我们需要定义一个配置接口服务(默认下载的ABP框架中没有公布这个接口定义和实现)。

如下我们定义一个SettingsAppService和他的接口

然后我们可以实现它的获取信息和修改信息的接口,如下所示是对系统级别的邮件参数进行配置管理。

        /// <summary>
        ///获取应用程序级别的邮件配置(系统邮件配置)/// </summary>
        /// <returns></returns>
        public async Task<EmailSettingsEditDto>GetEmailSettingsForApplication()
{
var smtpPassword = awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Password);return newEmailSettingsEditDto
{
DefaultFromAddress
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.DefaultFromAddress),
DefaultFromDisplayName
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.DefaultFromDisplayName),
SmtpHost
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Host),
SmtpPort
= await SettingManager.GetSettingValueForApplicationAsync<int>(EmailSettingNames.Smtp.Port),
SmtpUserName
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.UserName),
SmtpPassword
=SimpleStringCipher.Instance.Decrypt(smtpPassword),
SmtpDomain
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Domain),
SmtpEnableSsl
= await SettingManager.GetSettingValueForApplicationAsync<bool>(EmailSettingNames.Smtp.EnableSsl),
SmtpUseDefaultCredentials
= await SettingManager.GetSettingValueForApplicationAsync<bool>(EmailSettingNames.Smtp.UseDefaultCredentials)
};
}
/// <summary> ///更新应用程序级别的邮件配置(系统邮件配置)/// </summary> /// <returns></returns> public asyncTask UpdateEmailSettingsForApplication(EmailSettingsEditDto input)
{
awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.DefaultFromAddress, input.DefaultFromAddress);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.DefaultFromDisplayName, input.DefaultFromDisplayName);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Host, input.SmtpHost);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Port, input.SmtpPort.ToString(CultureInfo.InvariantCulture));awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.UserName, input.SmtpUserName);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Password, SimpleStringCipher.Instance.Encrypt(input.SmtpPassword));awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Domain, input.SmtpDomain);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.EnableSsl, input.SmtpEnableSsl.ToString().ToLowerInvariant());awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.UseDefaultCredentials, input.SmtpUseDefaultCredentials.ToString().ToLowerInvariant());
}

2、使用自定义的参数配置管理

我在较早的随笔《
Winform开发框架之参数配置管理功能实现-基于SettingsProvider.net的构建
》中介绍过对配置信息的管理实现,这种配置参数方式一直很好的应用在我的各个框架上,定义和使用都相对比较简单,能够满足绝大多数的应用场景,相对ABP框架的配置模块来说,简单易用。

首先我们定义一个用来存储通用配置信息的表,如下所示。

这个配置表的主要特点也是以键为操作对象,然后内容是JSON序列化后的内容,可以存储用户自定义的类的序列号字符串,这个是它的灵魂所在。和ABP框架仅仅存储简单类型的值有所不同。

和其他模块的定义一样,我们可以先根据常规表的方式,使用代码快速生成类的结构,如下所示。

    /// <summary>
    ///用户参数配置,应用层服务接口实现/// </summary>
[AbpAuthorize]public class UserParameterAppService : MyAsyncServiceBase<UserParameter, UserParameterDto, string, UserParameterPagedDto, CreateUserParameterDto, UserParameterDto>, IUserParameterAppService
{
private readonly IRepository<UserParameter, string>_repository;public UserParameterAppService(IRepository<UserParameter, string> repository) : base(repository)
{
_repository
=repository;
}

然后定义几个用于用户级别和系统程序级别的接口实现,如获取信息,修改信息等。

然后,在生成的Caller层类里面,增加以上的Web API接口调用的实现代码,如下所示

    /// <summary>
    ///用户参数配置的Web API调用处理/// </summary>
    public class UserParameterApiCaller : AsyncCrudApiCaller<UserParameterDto, string, UserParameterPagedDto, CreateUserParameterDto, UserParameterDto>, IUserParameterAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticUserParameterApiCaller Instance
{
get{return Singleton<UserParameterApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicUserParameterApiCaller()
{
this.DomainName = "UserParameter";//指定域对象名称,用于组装接口地址 }public async Task<UserParameterDto>GetSettingForUser(NameInputDto input)
{
return await DoActionAsync<UserParameterDto>(MethodBase.GetCurrentMethod(), input);
}
public asyncTask ChangeSettingForUser(NameValueDto input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}
public async Task<UserParameterDto>GetSettingForApplication(NameInputDto input)
{
return await DoActionAsync<UserParameterDto>(MethodBase.GetCurrentMethod(), input);
}
public asyncTask ChangeSettingForApplication(NameValueDto input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}
}

如果对于上面的DoActionAsyn的处理有疑问,可以参考之前随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》进行了解。

我在之前介绍过的配置模块里面,结合过FireFoxDialog界面效果,实现较好的参数配置管理功能,如下界面所示。

我们本次使用这两个不同的配置模块,也希望使用这个来展现一下,以便更好的理解。

由于整合了SettingsProvider.net组件,我们只需要封装一下对数据库的存储获取方式就可以了。

    /// <summary>
    ///数据库参数存储设置/// </summary>
    public classDatabaseStorage : JsonSettingsStoreBase
{
/// <summary> ///配置级别/// </summary> public SettingScopes Scope { get; set; }/// <summary> ///构造函数/// </summary> publicDatabaseStorage()
{
this.Scope =SettingScopes.User;
}
/// <summary> ///参数构造函数/// </summary> /// <param name="scope">配置级别</param> publicDatabaseStorage(SettingScopes scope)
{
this.Scope =scope;
}
/// <summary> ///保存到数据库/// </summary> /// <param name="filename">文件名称(类型名称)</param> /// <param name="fileContents">参数内容</param> protected override void WriteTextFile(string filename, stringfileContents)
{
var info = newNameValueDto(filename, fileContents);if (this.Scope ==SettingScopes.Application)
{
AsyncContext.Run(()
=>UserParameterApiCaller.Instance.ChangeSettingForApplication(info));
}
else{
AsyncContext.Run(()
=>UserParameterApiCaller.Instance.ChangeSettingForUser(info));
}
}
/// <summary> ///从数据库读取/// </summary> /// <param name="filename">文件名称(类型名称)</param> /// <returns></returns> protected override string ReadTextFile(stringfilename)
{
var info = newNameInputDto(filename);

UserParameterDto result
= null;if (this.Scope ==SettingScopes.Application)
{
result
= AsyncContext.Run(() =>UserParameterApiCaller.Instance.GetSettingForApplication(info));
}
else{
result
= AsyncContext.Run(() =>UserParameterApiCaller.Instance.GetSettingForUser(info));
}
return result != null ? result.Content : null;
}
}

有了这个实现,这样在操作上,就不用管理这些内容如何获取和更新了,和之前的使用配置管理方式一致了。可以处理各种不同的配置对象信息。

先来看看默认ABP的配置处理方式,管理界面如下所示。

这里的配置存储咋ABP的AbpSettings表里面,如下所示,每项内容是以字符串方式独立存储的。

它的调用主要就是SettingsApiCaller的内容了,注意这个邮件配置,必须在EmailSettingProvider中提前定义好对象的信息。

        privateEmailSettingsEditDto GetParameter()
{
EmailSettingsEditDto param
= AsyncContext.Run(() =>SettingsApiCaller.Instance.GetEmailSettingsForApplication());if(param == null)
{
param
= newEmailSettingsEditDto();
}
returnparam;
}
public override voidOnInit()
{
var parameter =GetParameter();if (parameter != null)
{
this.txtEmail.Text =parameter.DefaultFromAddress;this.txtLoginId.Text =parameter.SmtpUserName;this.txtPassword.Text =parameter.SmtpPassword;this.txtPassword.Tag =parameter.SmtpPassword;this.txtSmtpPort.Value =parameter.SmtpPort;this.txtSmtpServer.Text =parameter.SmtpHost;this.txtUseSSL.Checked =parameter.SmtpEnableSsl;
}
}

下面我们再来看看自定义的配置管理方式。如下是自定义配置模块获取显示的内容。

这个配置是系统级别的,它的获取方式如下所示。

    public partial classPageEmailApplication : PropertyPage
{
privateSettingsProvider settings;privateISettingsStorage store;publicPageEmailApplication()
{
InitializeComponent();
if (!this.DesignMode)
{
store
= newDatabaseStorage(SettingScopes.Application);
settings
= newSettingsProvider(store);
}
}
public override voidOnInit()
{
EmailParameter parameter
= settings.GetSettings<EmailParameter>();if (parameter != null)
{
this.txtEmail.Text =parameter.Email;this.txtLoginId.Text =parameter.LoginId;this.txtPassword.Text =parameter.Password;this.txtPassword.Tag =parameter.Password;this.txtPop3Port.Value =parameter.Pop3Port;this.txtPop3Server.Text =parameter.Pop3Server;this.txtSmtpPort.Value =parameter.SmtpPort;this.txtSmtpServer.Text =parameter.SmtpServer;this.txtUseSSL.Checked =parameter.UseSSL;
}
}

以上是标准的SettingsProvider.net的组件调用方式,我们不用知道具体的数据存储,只需要把内容直接GetSetting方式获取出来即可。

而保存内容,直接通过使用SaveSettings保存即可。

                EmailParameter parameter = settings.GetSettings<EmailParameter>();if (parameter != null)
{
parameter.Email
= this.txtEmail.Text;
parameter.LoginId
= this.txtLoginId.Text;
parameter.Password
= this.txtPassword.Text;
parameter.Pop3Port
= Convert.ToInt32(this.txtPop3Port.Value);
parameter.Pop3Server
= this.txtPop3Server.Text;
parameter.SmtpPort
= Convert.ToInt32(this.txtSmtpPort.Value);
parameter.SmtpServer
= this.txtSmtpServer.Text;
parameter.UseSSL
= this.txtUseSSL.Checked;

settings.SaveSettings
<EmailParameter>(parameter);
}

其中 EmailParameter 类是我们定义的一个类,用来承载相关的配置信息,如下所示。它支持默认值,加密处理等设置。

    /// <summary>
    ///邮箱设置/// </summary>
    public classEmailParameter
{
/// <summary> ///邮件账号/// </summary> //[DefaultValue("wuhuacong@163.com")] public string Email { get; set; }/// <summary> ///POP3服务器/// </summary> [DefaultValue("pop.163.com")]public string Pop3Server { get; set; }/// <summary> ///POP3端口/// </summary> [DefaultValue(110)]public int Pop3Port { get; set; }/// <summary> ///SMTP服务器/// </summary> [DefaultValue("smtp.163.com")]public string SmtpServer { get; set; }/// <summary> ///SMTP端口/// </summary> [DefaultValue(25)]public int SmtpPort { get; set; }/// <summary> ///登陆账号/// </summary> public string LoginId { get; set; }/// <summary> ///登陆密码/// </summary> [ProtectedString]public string Password { get; set; }/// <summary> ///使用SSL加密/// </summary> [DefaultValue(false)]public bool UseSSL { get; set; }
}

由于SettingsProvider.net组件的支持,我们还可以把配置信息当成本地文件存储起来,对于一些需要存为文件的方式的配置,非常不错。

    public partial classPageReport : PropertyPage
{
privateSettingsProvider settings;privateISettingsStorage store;publicPageReport()
{
InitializeComponent();
if (!this.DesignMode)
{
//PortableStorage: 在运行程序目录创建一个setting的文件记录参数数据 store = newPortableStorage();
settings
= newSettingsProvider(store);
}
}

以上就是介绍了ABP配置管理模块的实现原理和客户端的调用,以及使用自定义配置管理模块的方式进行处理更加动态化或者灵活一点的配置信息,使用自定义配置信息管理服务,整合了SettingProvider.net的支持,可以实现更好的参数配置管理体验。

在一般的检索界面中,基于界面易用和美观方便的考虑,我们往往只提供一些常用的条件查询进行列表数据的查询,但是有时候一些业务表字段很多,一些不常见的条件可能在某些场景下也需要用到。因此我们在通用的查询条件之外,一般可以考虑增加 一个高级查询的模块来管理这些不常见条件的查询处理。本篇随笔基于这个需求,综合ABP框架的特点,整合了高级查询模块功能的处理。

1、高级查询模块的回顾

我们知道,在界面布局中,一般常见的查询条件不能太多,否则会显得臃肿而且占用太多空间,非常不美观,因此常见的查询都是提供寥寥几个的输出条件进行列表记录的查询的。

又或者一些更多内容的界面,我们也是仅仅提供多几个条件,其他的想办法通过高级查询界面进行查询管理。

在早期博客里面《
Winform开发框架之通用高级查询模块
》,我曾经介绍过一款通用的高级查询界面处理,用在Winform框架里面,可以对数据表更多的字段进行统一的查询处理。

对于内容较多的查询,我们可以在主界面增加一个高级查询按钮入口,如上图所示,单击后,显示一个所有字段的列表,如下界面。

一般来说,查询条件分为文本输入,如姓名,邮件,名称等这些。

日期类型条件输入界面:

数字类型条件输入界面:

输入以上几种条件后,高级查询界面里面会显示友好的条件内容,确保用户能够看懂输入的条件,如下所示是输入几个不同类型的条件的显示内容。

以上是高级查询模块的思路,整体界面和处理逻辑虽然可以采用,但是在ABP框架模式下,以前的处理方式有所不同了,下面详细介绍一下如何在ABP框架模块下整合这个高级查询模块的内容。

2、ABP框架模块下的高级查询处理

我们先来了解一下最终在ABP框架下整合的高级查询模块界面如下所示。

可以设置一些模糊查询条件,以及一些区间的查询值,如下所示。

这个模块是以ABP框架的Web API获取数据,并通过Winform界面进行调用,从而形成了一个ABP+Winform的框架体系。

前面ABP框架系列介绍过,我们一般使用GetAll和分页条件DTO进行数据的检索,如下是产品分页DTO的定义

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public class ProductPagedDto : PagedAndSortedInputDto

而PagedAndSortedInputDto也是自定义的类,它主要用来承载一些分页和排序的信息,如下所示

    /// <summary>
    ///带有排序对象的分页基类/// </summary>
    public classPagedAndSortedInputDto : PagedInputDto, ISortedResultRequest
{
/// <summary> ///排序信息/// </summary> public string Sorting { get; set; }

其中的PagedInputDto也是自定义类,主要承载分页信息。

    /// <summary>
    ///分页对象/// </summary>
    public classPagedInputDto : IPagedResultRequest
{
[Range(
1, int.MaxValue)]public int MaxResultCount { get; set; }

[Range(
0, int.MaxValue)]public int SkipCount { get; set; }publicPagedInputDto()
{
MaxResultCount
= int.MaxValue;
}
}

这样的构建,我们可以传递分页和排序信息,因此在GetAll函数里面,就可以根据这些条件进行数据查询了。

而我们通过重写过滤条件和排序处理,就可以实现数据的分页查询了。对于产品信息的过滤处理和排序处理,我们重写函数如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Product>CreateFilteredQuery(ProductPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProductNo.IsNullOrWhiteSpace(), t => t.ProductNo.Contains(input.ProductNo)) //如需要精确匹配则用Equals .WhereIf(!input.BarCode.IsNullOrWhiteSpace(), t => t.BarCode.Contains(input.BarCode)) //如需要精确匹配则用Equals .WhereIf(!input.MaterialCode.IsNullOrWhiteSpace(), t => t.MaterialCode.Contains(input.MaterialCode)) //如需要精确匹配则用Equals .WhereIf(!input.ProductType.IsNullOrWhiteSpace(), t => t.ProductType.Contains(input.ProductType)) //如需要精确匹配则用Equals .WhereIf(!input.ProductName.IsNullOrWhiteSpace(), t => t.ProductName.Contains(input.ProductName)) //如需要精确匹配则用Equals .WhereIf(!input.Unit.IsNullOrWhiteSpace(), t => t.Unit.Contains(input.Unit)) //如需要精确匹配则用Equals .WhereIf(!input.Note.IsNullOrWhiteSpace(), t => t.Note.Contains(input.Note)) //如需要精确匹配则用Equals .WhereIf(!input.Description.IsNullOrWhiteSpace(), t => t.Description.Contains(input.Description)) //如需要精确匹配则用Equals//状态 .WhereIf(input.Status.HasValue, t => t.Status==input.Status)//成本价区间查询 .WhereIf(input.PriceStart.HasValue, s => s.Price >=input.PriceStart.Value)
.WhereIf(input.PriceEnd.HasValue, s
=> s.Price <=input.PriceEnd.Value)//销售价区间查询 .WhereIf(input.SalePriceStart.HasValue, s => s.SalePrice >=input.SalePriceStart.Value)
.WhereIf(input.SalePriceEnd.HasValue, s
=> s.SalePrice <=input.SalePriceEnd.Value)//特价区间查询 .WhereIf(input.SpecialPriceStart.HasValue, s => s.SpecialPrice >=input.SpecialPriceStart.Value)
.WhereIf(input.SpecialPriceEnd.HasValue, s
=> s.SpecialPrice <=input.SpecialPriceEnd.Value)
.WhereIf(input.IsUseSpecial.HasValue, t
=> t.IsUseSpecial == input.IsUseSpecial) //如需要精确匹配则用Equals//最低折扣区间查询 .WhereIf(input.LowestDiscountStart.HasValue, s => s.LowestDiscount >=input.LowestDiscountStart.Value)
.WhereIf(input.LowestDiscountEnd.HasValue, s
=> s.LowestDiscount <=input.LowestDiscountEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<Product> ApplySorting(IQueryable<Product>query, ProductPagedDto input)
{
//按创建时间倒序排序 return base.ApplySorting(query, input).OrderByDescending(s => s.CreationTime);//时间降序 }

虽然我们一般在界面上不会放置所有的条件,但是高级查询模块倒是可以把分页条件DTO里面的条件全部摆上去的。

高级查询模块的条件如下所示。

我们高级查询里面的条件还是以GetAll里面的对象分页查询Dto里面的属性,我们需要根据这些条件进行构建,也需要以这些属性的类型进行一个控件的选择。

因此我们需要一个属性的名称说明,以及在高级查询模块的列表界面中对显示那些字段进行控制,如下代码所示。

        privateFrmAdvanceSearch dlg;/// <summary>
        ///高级查询的操作/// </summary>        
        private async voidAdvanceSearch()
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.SetFieldTypeList
<ProductPagedDto>();//通过分页对象获取查询属性和类型 dlg.ColumnNameAlias = awaitProductApiCaller.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "ProductNo,BarCode,MaterialCode,ProductType,ProductName,Unit,Price,SalePrice,SpecialPrice,IsUseSpecial,LowestDiscount,Note,Description,Status,CreatorUserId,CreationTime";

通过 SetFieldTypeList<ProductPagedDto> 的处理,我们把分页对象的查询属性和类型赋值给了高级查询模块,让它根据类型来创建不同的输入显示,如常规的字符串、数值区段、日期区段,下拉列表等等。

对于下拉列表,我们需要绑定它的数据源,如下代码所示。

 dlg.AddColumnListItem("ProductType", await DictItemUtil.GetDictListItemByDictType("产品类型"));//字典列表
 dlg.AddColumnListItem("Status", await DictItemUtil.GetDictListItemByDictType("产品状态"));//字典列表

而对于一些常规的固定列表,也可以以类似的方式加入下拉列表

    //固定转义的列表
    var specialList = new List<CListItem>() { new CListItem("特价", "True"), new CListItem("一般", "False") };
dlg.AddColumnListItem(
"IsUseSpecial", specialList);

或者

    dlg.AddColumnListItem("Sex", "男,女");//固定列表

因此整个调用高级查询模块的代码如下所示

    privateFrmAdvanceSearch dlg;/// <summary>
    ///高级查询的操作/// </summary>        
    private async voidAdvanceSearch()
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.SetFieldTypeList
<ProductPagedDto>();//通过分页对象获取查询属性和类型 dlg.ColumnNameAlias = awaitProductApiCaller.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "ProductNo,BarCode,MaterialCode,ProductType,ProductName,Unit,Price,SalePrice,SpecialPrice,IsUseSpecial,LowestDiscount,Note,Description,Status,CreatorUserId,CreationTime";#region 下拉列表数据dlg.AddColumnListItem("ProductType", await DictItemUtil.GetDictListItemByDictType("产品类型"));//字典列表 dlg.AddColumnListItem("Status", await DictItemUtil.GetDictListItemByDictType("产品状态"));//字典列表//固定转义的列表 var specialList = new List<CListItem>() { new CListItem("特价", "True"), new CListItem("一般", "False") };
dlg.AddColumnListItem(
"IsUseSpecial", specialList);//dlg.AddColumnListItem("Sex", "男,女");//固定列表//dlg.AddColumnListItem("Credit", await ProductApiCaller.Instance.GetFieldList("Credit"));//动态列表 #endregiondlg.ConditionChanged+= newFrmAdvanceSearch.ConditionChangedEventHandler(dlg_ConditionChanged);
}
dlg.ShowDialog();
}

在处理获取数据GetData函数的时候,我们需要根据高级查询进行一定的切换,以便显示正确的过滤条件,如下代码所示是获取数据的处理。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<ProductDto>>GetData()
{
ProductPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new ProductPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else{//构建分页的条件和查询条件 pagerDto = new ProductPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 ProductNo = this.txtProductNo.Text.Trim(),
BarCode
= this.txtBarCode.Text.Trim(),
MaterialCode
= this.txtMaterialCode.Text.Trim(),
ProductType
= this.txtProductType.Text.Trim(),
ProductName
= this.txtProductName.Text.Trim(),
Description
= this.txtDescription.Text.Trim(),
};
//日期和数值范围定义//创建时间,需在ProductPagedDto中添加DateTime?类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreationTimeStart =CreationTime.Start;
pagerDto.CreationTimeEnd
=CreationTime.End;

}
var result = awaitProductApiCaller.Instance.GetAll(pagerDto);returnresult;
}

在高级查询的处理方式下,我们是传入一个列表的分页对象属性,然后传入一个分页DTO对象,就可以构建出我们需要的分页查询条件,传递给Web API端获取对应条件的数据了。

    pagerDto = new ProductPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
= dlg.GetPagedResult(pagerDto);

而高级查询模块,所需要处理的逻辑就是需要根据不同的属性类型,赋值常规的属性值或者区段属性值,从而构建出分页对应的属性条件即可。

如果是区段(包括日期或者数值)的,我们分页查询条件里面,会有一个ABCStart,ABCEnd的对象属性,依照这个规则,获取到对应的用户输入,采用反射方式赋值DTO对象即可。

前面介绍了很多ABP系列的文章,一步一步的把我们日常开发中涉及到的Web API服务构建、登录日志和操作审计日志、字典管理模块、省份城市的信息维护、权限管理模块中的组织机构、用户、角色、权限、菜单等内容,以及配置管理模块,界面的高级查询处理等内容,同时我们把整个开发理念结合我们的代码生成工具Database2Sharp进行快速的开发,快速开发分为两个部分:一个是Web API部分的ABP框架代码生成,包括领域实体层、应用服务层、公用DTO和接口层、API调用Caller层等内容的生成,并且整合了我们封装的基类,以便生成的代码更加简洁,但却具有更加强大、易用的功能函数;另一个快速生成的部分是界面生成,我们根据我们在Winform领域多年的开发,整合了分页、公用类库和Winform基础界面类的基础上实现了快速的Winform界面生成,并且Winform界面生成可以根据需要配置列表查询条件、列表显示内容,编辑界面的字段显示等等内容,并且生成的Winform界面自动具有和ABP快速框架整体调用通用规则。

1、ABP框架背景知识介绍

ABP
是ASP.NET Boilerplate的简称,ABP是一个
开源
且文档友好的
应用程序框架
。ABP不仅仅是一个框架,它还提供了一个
最徍实践
的基于
领域驱动设计(DDD)
的体系结构模型。学习使用ABP框架也有一段时间了,一直想全面了解下这个框架的整个来龙去脉,并把想把它使用历程整理成一个系列出来,不过一直没有下笔来写这篇文章的开篇,就是希望能够深入了解,再深入了解一些,希望自己能够理解透彻一些,不能误人子弟,也不想和网上千篇一律的翻译官网的内容,官网的英文介绍也已经很详细了,于是我觉得还是以实际使用的过程进行一定的整理会更好。

初次了解ABP框架,对它还是非常惊艳的,它基本上是.NET 领域架构的集大成者,几乎囊括了我们.NET领域排的上名的各种技术应用,而且它本身可以支持.net framework和.net core两种技术流派,对它的介绍也是非常感兴趣。

1)ABP框架的特点

我们来大概了解下ABP框架涉及到的内容。

  • 依赖注入
    ,这个部分使用 Castle windsor (依赖注入容器)来实现依赖注入,这个也是我们经常使用IOC来处理的方式;
  • Repository仓储模式
    ,已实现了Entity Framework、NHibernate、MangoDB、内存数据库等,仓储模式可以快速实现对数据接口的调用;
  • 身份验证与授权管理
    ,可以使用声明特性的方式对用户是否登录,或者接口的权限进行验证,可以通过一个很细粒度的方式,对各个接口的调用权限进行设置;
  • 数据有效性验证
    ,ABP自动对接口的输入参数对象进行非空判断,并且可以根据属性的申请信息对属性的有效性进行校验;
  • 审计日志记录
    ,也就是记录我们对每个接口的调用记录,以及对记录的创建、修改、删除人员进行记录等处理;
  • Unit Of Work工作单元模式
    ,为应用层和仓储层的方法自动实现数据库事务,默认所有应用服务层的接口,都是以工作单元方式运行,即使它们调用了不同的存储对象处理,都是处于一个事务的逻辑里面;
  • 异常处理
    ,ABP框架提供了一整套比较完善的流程处理操作,可以很方便的对异常进行进行记录和传递;
  • 日志记录
    ,我么可以利用Log4Net进行常规的日志记录,方便我们跟踪程序处理信息和错误信息;
  • 多语言/本地化支持
    ,ABP框架对多语言的处理也是比较友好的,提供了对XML、JSON语言信息的配置处理;
  • Auto Mapping自动映射
    ,这个是ABP的很重要的对象隔离概念,通过使用AutoMaper来实现域对象和DTO对象的属性映射,可以隔离两者的逻辑关系,但是又能轻松实现属性信息的赋值;
  • 动态Web API层
    ,利用这个动态处理,可以把Application Service 直接发布为Web API层,而不需要在累赘的为每个业务对象手工创建一个Web API的控制器,非常方便;
  • 动态JavaScript的AJax代理处理
    ,可以自动创建Javascript 的代理层来更方便使用Web Api,这个在Web层使用。

除了这些重要特性外,ABP框架还有很多一些特别的功能或者概念。

  • 多租户支持(每个租户的数据自动隔离,业务模块开发者不需要在保存和查询数据时写相应代码;
  • 软删除支持(继承相应的基类或实现相应接口,会自动实现软删除)
  • 系统设置存取管理(系统级、租户级、用户级,作用范围自动管理)
  • EventBus实现领域事件(Domain Events)
  • 模块以及模块的依赖关系实现插件化的模块处理等等

ABP框架主要还是基于领域驱动的理念来构建整个架构的,其中领域驱动包含的概念有 域对象Entities、仓储对象Repositories、域服务接口层Domain Services、域事件Domain Events、应用服务接口Application Services、数据传输对象DTOs等。

ABP官方网站:http://www.aspnetboilerplate.com,从里面可以查看很详细的案例和文档说明,可以根据需要下载不同类型的基础框架。

ABP GitHub源码地址:https://github.com/aspnetboilerplate,可以下载整个基础的框架内容,以及相关的样板案例代码。

基于ABP框架基础上,我们整理完善了整个权限体系,以及基于这个基础上进行的业务系统快速开发,我们整理后的ABP快速开发框架的架构图示,如下图所示(以字典模块为例说明)

针对Web API接口调用的封装,为了适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

2、基于Winform的ABP快速开发框架功能介绍

1)登陆界面

2)主体框架界面

主体框架界面采用的是基于菜单的动态生成,以及多文档的界面布局,具有非常好的美观性和易用性。

左侧的功能树列表和顶部的菜单模块,可以根据角色拥有的权限进行动态构建,不同的角色具有不同的菜单功能点,如下是测试用户登录后具有的界面。

3)用户管理界面

用户列表包括分页查询、导入、导出用户数据,以及可以利用右键菜单进行新增、编辑用户记录,或者对指定用户进行重置密码操作。

用户编辑界面如下所示。

当然可以查看这个用户本身拥有的权限功能点,如下界面所示。

用户数据导入 ,可以根据指定模板的格式进行填写后,然后导入指定的文件内容,如下所示。

导出文件则是把列表中现有的数据进行导入,导出后提示是否打开Excel文件进行查看。

4)组织机构管理

组织机构主要就是一个层级的对象关系,一般包含但不限于公司、部门、工作组等的定义,其中组织机构包含用户成员和角色成员的关系,如下界面所示。

组织机构的树形列表可以进行拖动实现不同层级的变化

或者通过右键菜单进行编辑修改操作

组织机构可以修改机构名称和对应的父类节点,如下界面所示。

组织机构包含的成员可以添加多个人员记录,添加界面如下所示。

添加角色界面如下所示。

5)角色管理

角色信息没有层级关系,可以通过列表展示。

其中角色包含权限分配、角色成员和拥有菜单的维护,如下是角色编辑界面,包含角色基本信息、拥有权限、包含成员、拥有菜单的管理等。菜单对于角色来说,应该是一种界面资源,可以通过配置进行管理对应角色用户的菜单。

菜单编辑界面如下所示。

角色的权限包含系统可以用的权限,并可以勾选为角色设置所需的功能点,如下界面所示。

用户成员则和机构的用户管理一样,可以指定多个用户,这里不再赘述。

而菜单对于角色来说,应该是一种界面资源,可以通过配置进行管理对应角色用户的菜单,如下界面所示。

通过配置好的菜单,用户登录系统后,系统根据当前用户具有的菜单项目,动态构建显示对应的列表菜单和工具栏菜单。

6)功能管理

严格来说,ABP框架并没有统一管理好权限功能点的,它没有任何表来存储这个功能集合,而是通过派生AuthorizationProvider的子类来定义权限功能点

我在这个基础上引入了一个权限功能的表用来存储功能点的,然后提供管理界面来动态维护这些功能点。如下界面所示。

我们刚才在角色里面看到可以分配的权限内容,就是基于这个权限表的信息展示。

这样我们可以动态添加或者批量添加所需要的功能点,并且和整个权限管理模块串联起来,形成一个完整的控制体系。

另外我们还可以通过左侧树列表的右键菜单管理列表,如下所示可以级联删除对应的节点及其下面所有子节点。

系统登录后,客户端自动获取对应用户的角色功能点,然后我们每次打开一个新的业务窗体,客户端会进行界面的权限逻辑控制,如果没有权限的,那么不可以访问操作,如下是禁止了产品信息的导入、导出、新增、编辑等操作功能,如下界面所示产品界面被动态取消相关权限后,界面禁止了某些操作功能。

7)菜单管理

Winform主界面的开发,基本上都是标准的界面,在顶部放置工具栏,中间区域则放置多文档的内容,但是在顶部菜单比较多的时候,就需要把菜单分为几级处理,如可以在顶部菜单放置一二级菜单,这种方式在一般功能点不算太多的情况下,呈现的界面效果较为直观、也较为美观。不过随着一些系统功能的增多,这种方式可能就会显得工具栏比较拥挤,那么我们是否可以在左侧放置一个树形列表,这样通过树形列表的收缩折叠,就可以放置非常多的菜单功能了。

在ABP快速开发框架里面,我们依旧采用Winform开发框架里面用到的菜单维护方式,在权限模块系统中维护菜单内容,并在用户登录系统后,动态加载菜单展示,并通过菜单的配置信息,可以动态展示不同的窗体信息。

菜单资源管理的列表界面界面如下所示

左侧列表依旧可以通过右键菜单进行维护管理。

双击树列表或者右侧的列表,都可以对已有的菜单进行编辑,菜单编辑界面如下所示。

我们可以通过选择图标按钮进行菜单图标的选择,如下是选择菜单图片的界面。

这样我们可以采用DevExpress内置漂亮的系统图标,也可选择系统文件里面的图标文件。

如果用户登录系统后,觉得自己管理的菜单有些多,那么可以通过菜单配置的方式,选择性的呈现某些菜单,把其他部分的菜单隐藏起来,这个就是自定义应用菜单界面,界面如下所示。

8)系统登录日志

登录日志,这个就是用户尝试登录的时候,留下的记录信息,其中包括用户的登录用户名,ID,IP地址、登录时间,以及登录是否成功的状态等信息。

这个登录日志,就是在系统登录尝试的时候,留下的记录,可供管理员进行跟踪了解某个账户的使用情况,也可以根据这些登录信息进行一个统计报表的分析。

9)系统审计日志

审计日志,设置我们在访问或者调用某个应用服务层接口的时候,横切面流下的一系列操作记录,其中记录我们访问的服务接口,参数,客户端IP地址,访问时间,以及异常等信息,这些操作都是在ABP系统自动记录的,如果我们需要屏蔽某些服务类或者接口,则这些就不会记录在里面,否则默认是记录的。

审计日志的明细展示界面如下所示。

10)字典管理

字典管理界面,左侧列出字典类型,并对字典类型下的字典数据进行分页展示,右侧则利用分页展示对应字典类型的字典数据,字典管理界面如下所示。

新增或者编辑窗体界面如下

批量添加字典内容的界面如下所示。

省份城市行政区的模块管理,也主要是为了提供一个三级联动的字典列表给界面使用,因此这里对这些统计局的数据进行一个维护和展示出来,如下界面所示。

11)参数配置模块

一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查询来使用,本ABP快速开发框架引入了另外一种配置信息的定义,实现更加简化的处理。

参数配置管理模块界面如下所示。

配置模块使用主要特点是以键为操作对象,然后内容是JSON序列化后的内容,可以存储用户自定义的类的序列号字符串,通过整合了SettingsProvider.net组件,使得我们操作配置内容更加方便和动态化。

12) 附件管理模块

该模块其实是很通用的一个模块,例如我们的一些日常记录,可能会伴随着有图片、文档等的附件管理,如果为每个业务对象都做一个附件管理,或者每次开发系统都重新做,那么效率肯定没有直接采用通用的附件管理那么方便快捷了。

其实这个模块是基于我的Winform开发框架开发的,同样现在的ABP快速开发框架的Winform端依旧可以使用这些模块的界面和大概的处理规则,我们把它利用ABP框架的获取数据方式调整一下即可。

首先我们创建一个独立的控件,用于放到编辑数据记录窗口里面,如下所示。

这样在项目中集成(如数据编辑窗口),直接拖动这个控件到界面中,运行就可以看到下面的效果了。

由于一般创建记录的时候,给他指定一个GUID的附件组ID,这样我们在数据记录保存前,我们就可以上传附件了,如下所示。

而且在这个过程中,可以随时查看自己在该记录中已经上传的附件。如果附件不够,可以随时启动上传操作,附件支持多选文件,然后一次性,启动后台线程操作方式,把文件上传及附件记录保存到数据库。

由于是附件管理,因此有可能上传各种文件,包括Word文档、Excel文档、压缩文件,以及各种类型的图片,因此为了方便对图片的查看,这个控件集成了图片查看控件,可以非常方便直接读取图片附件的数据流作为对象展示,该图片控件支持对图片的滚动放大缩小、左右翻转、选择放大、图片移动、保存图片等功能,不需要查看,直接使用ESC退出即可。

附件管理在很多信息化管理系统中很普遍使用,例如我在病人管理系统界面里面,可以在一个界面里面分门别类管理很多影像学的图片资料,通过查看附件,可以看到其中一些图片附件的缩略图,需要进一步查看,可以双击图片即可实现预览效果。

通用附件管理模块,就是基于这些特点,实现图片、Excel文档、Word文档和PDF等文档的在线预览和管理操作,界面截图如下所示。

以上就是框架里面主要的模块内容展示,当然我们可以结合代码生成工具来快速开发自己的业务管理模块,而这些主要就是设计好数据库后,对框架代码和界面代码的统一生成后进行整合到主体框架里面即可,可以获得高效、统一、快速的开发体验。

详细的代码生成工具开发过程,可以了解随笔《
利用代码生成工具生成基于ABP框架的代码
》、《
ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程

ABP框架代码生成

最终根据根据选择数据库表信息,一键生成相关ABP架构分层代码,文件结构如下所示。

ABP框架的Winform界面开发

设置好后直接生成,代码工具就可以依照模板来生成所需要的WInform列表界面和编辑界面的内容了,如下是生成的界面代码。

放到VS项目里面,就看到对应的窗体界面效果了。

生成界面后,进行一定的布局调整就可以实际用于生产环境了,省却了很多时间。

系列文章列表如下所示: