ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用

原文:https://www.cnblogs.com/wuhuacong/p/10949741.html


在前面几篇随笔介绍了我对ABP框架的改造,包括对ABP总体的介绍,以及对各个业务分层的简化,Web API 客户端封装层的设计,使得我们基于ABP框架的整体方案越来越清晰化, 也越来越接近实际的项目开发需求,一旦整个模式比较成熟,并以一种比较固化的模式来指导开发,那么就可以很方便的应用在实际项目开发当中了。本篇随笔是基于前面几篇的基础上,在Winform项目上进一步改造为实际项目的场景,把我原来基于微软企业库底层的数据库访问方式的Winform框架或者混合框架的字典模块界面改造为基于ABP框架基础上的字典应用模块。

1)APICaller层接口的回顾

在上一篇随笔《ABP开发框架前后端开发系列---(4)Web API调用类的封装和使用》中,我介绍了Web API调用类的封装和使用,并介绍了在.net 控制台程序中,测试对ApiCaller层的调用,并能够顺利返回我们所需要的数据。测试代码如下所示。

    #region DictType

    using (var client = bootstrapper.IocManager.ResolveAsDisposable<DictTypeApiCaller>())
    {
        var caller = client.Object;

        Console.WriteLine("Logging in with TOKEN based auth...");
        var token = caller.Authenticate("admin", "123qwe").Result;
        Console.WriteLine(token.ToJson());

        caller.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token.AccessToken));

        Console.WriteLine("Get All ...");
        var pagerDto = new DictTypePagedDto() { SkipCount = 0, MaxResultCount = 10 };
        var result = caller.GetAll(pagerDto).Result;
        Console.WriteLine(result.ToJson());

        Console.WriteLine("Get All by condition ...");
        var pagerdictDto = new DictTypePagedDto() { Name = "民族" };
        result = caller.GetAll(pagerdictDto).Result;
        Console.WriteLine(result.ToJson());
        
        Console.WriteLine("Get count by condition ...");
        pagerdictDto = new DictTypePagedDto() {};
        var count = caller.Count(pagerdictDto).Result;
        Console.WriteLine(count);
        Console.WriteLine();

        Console.WriteLine("Create DictType...");
        var createDto = new CreateDictTypeDto { Id = Guid.NewGuid().ToString(), Name = "Test", Code = "Test" };
        var dictDto = caller.Create(createDto).Result;
        Console.WriteLine(dictDto.ToJson());

        Console.WriteLine("Update DictType...");
        dictDto.Code = "testcode";
        var updateDto = caller.Update(dictDto).Result;
        Console.WriteLine(updateDto.ToJson());

        if (updateDto != null)
        {
            Console.WriteLine("Delete DictType...");
            caller.Delete(new EntityDto<string>() { Id = dictDto.Id });
        }

    }
    #endregion

这些ApiCaller对象的接口测试代码,包括了授权登录,获取所有记录,获取条件查询记录,创建、更新、删除这些接口都成功执行,验证了我们对整体架构的设计改良,并通过对ApiCaller层基类的设计,减少我们对常规增删改查接口的编码,我们只需要编写我们的自定义业务接口代码封装类即可。

其中基类的代码如下所示。

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

也就是说,上面我们全部是基于基类接口的调用,还不需要为我们自定义接口编写任何一行代码,已经具备了常规的各种查询和数据处理功能了。

我们完整的字典类型ApiCaller类的代码如下所示。

namespace MyProject.Caller
{
    /// <summary>
    /// 字典类型对象的Web API调用处理
    /// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
    {
        /// <summary>
        /// 提供单件对象使用
        /// </summary>
        public static DictTypeApiCaller Instance
        {
            get
            {
                return Singleton<DictTypeApiCaller>.Instance;
            }
        }

        /// <summary>
        /// 默认构造函数
        /// </summary>
        public DictTypeApiCaller()
        {
            this.DomainName = "DictType";//指定域对象名称,用于组装接口地址
        }

        public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?dictTypeId={0}", dictTypeId);

            var result = await apiClient.GetAsync<Dictionary<string, string>>(url);
            return result; 
        }

        public async Task<IList<DictTypeNodeDto>> GetTree(string pid)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?pid={0}", pid);

            var result = await apiClient.GetAsync<IList<DictTypeNodeDto>>(url);
            return result;
        }
    }

这里面的函数定义才是我们需要根据实际的自定义接口封装的调用类函数代码。

前面我们介绍了,我们把ApiCaller层的项目设计为.net Standard的类库项目,因此可以在.net core或者在.net framework中进行使用,并且也在基于.net core的控制台程序中测试成功了。

下面就重点介绍一下,基于.net framework的Winfrom程序中对ABP框架的Web API接口的调用,如果以后Winform支持.net core了(据说9月份出的.net core3就包含了),那么也一样的模式进行调用。

 

2)Winform对ApiCaller层的调用

我们先来看看字典模块,通过封装对ABP框架的Web API调用后,实际的功能界面效果吧。

先设计一个授权登录的界面获取访问令牌信息。

字典管理界面,列出字典类型,并对字典类型下的字典数据进行分页展示,分页展示利用分页控件展示。

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

这个界面是来自于我的框架里面的字典模块界面,不过里面对数据的处理代码确实已经更改为适应ABP框架的Web API接口的调用的了(基于ApiCaller 层的调用)。

我们下面来一一进行分析即可。

登陆界面,我们看看主要的逻辑就是调用获取授权令牌的接口,并存储起来供后续界面中的业务类进行调用即可。

由于我们自己封装的ApiCaller类,都是基于异步的方式封装的,因此我们可以看到很多地方调用都使用await的关键字,这个是异步调用的关键字,如果方法需要定义为异步,就需要增加async关键字,一般这两个关键字是配套使用的。

如果我们在事件处理代码里面使用了异步,那么事件的函数也需要标记为async,如下是字典管理模块窗体的加载函数,也是用了async声明 和await调用异步方法标记。

        private async void FrmDictionary_Load(object sender, EventArgs e)
        {
            await InitTreeView();

            this.lblDictType.Text = "";
            await BindData();
            
            //分页控件事件处理代码
            this.winGridViewPager1.OnPageChanged += new EventHandler(winGridViewPager1_OnPageChanged);
            this.winGridViewPager1.OnStartExport += new EventHandler(winGridViewPager1_OnStartExport);
            this.winGridViewPager1.OnEditSelected += new EventHandler(winGridViewPager1_OnEditSelected);
            this.winGridViewPager1.OnAddNew += new EventHandler(winGridViewPager1_OnAddNew);
            this.winGridViewPager1.OnDeleteSelected += new EventHandler(winGridViewPager1_OnDeleteSelected);
            this.winGridViewPager1.OnRefresh += new EventHandler(winGridViewPager1_OnRefresh);
            this.winGridViewPager1.AppendedMenu = this.contextMenuStrip2;

            this.winGridViewPager1.BestFitColumnWith = false;
            this.winGridViewPager1.gridView1.DataSourceChanged += new EventHandler(gridView1_DataSourceChanged);
        }

我们的数据,主要是在BindData里面实现,这个函数是我们自己加的,由于使用了异步方法,因此也用async进行声明。

整个对于分页的数据获取和控件的数据绑定过程,代码如下所示。

        /// <summary>
        /// 获取数据
        /// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<DictDataDto>> GetData()
        {
            //构建分页的条件和查询条件
            var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
            {
                DictType_ID = string.Concat(this.lblDictType.Tag)
            };
            var result = await DictDataApiCaller.Instance.GetAll(pagerDto);
            return result;
        }

        /// <summary>
        /// 绑定数据
        /// </summary>
        private async Task BindData()
        {
            #region 添加别名解析
            this.winGridViewPager1.DisplayColumns = "Name,Value,Seq,Remark,EditTime";
            this.winGridViewPager1.AddColumnAlias(Id_FieldName, "编号");
            this.winGridViewPager1.AddColumnAlias("DictType_ID", "字典大类");
            this.winGridViewPager1.AddColumnAlias("Name", "项目名称");
            this.winGridViewPager1.AddColumnAlias("Value", "项目值");
            this.winGridViewPager1.AddColumnAlias("Seq", "字典排序");
            this.winGridViewPager1.AddColumnAlias("Remark", "备注");
            this.winGridViewPager1.AddColumnAlias("Editor", "修改用户");
            this.winGridViewPager1.AddColumnAlias("EditTime", "更新日期");
            #endregion

            if (this.lblDictType.Tag != null)
            {
                var result = await GetData();

                //设置所有记录数和列表数据源
                this.winGridViewPager1.DataSource = result.Items;
                this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount;
            }
        }

其中注意的是GetAll方式是传入一个条件查询的对象,这个就是DictDataPagedDto是我们定义的,放入我们DictDataDto里面的常见属性,方便我们根据属性匹配精确或者模糊查询。

    /// <summary>
    /// 用于根据条件查询
    /// </summary>
    public class DictDataPagedDto : PagedResultRequestDto
    {
        /// <summary>
        /// 字典类型ID
        /// </summary>
        public virtual string DictType_ID { get; set; }

        /// <summary>
        /// 类型名称
        /// </summary>
        public virtual string Name { get; set; }

        /// <summary>
        /// 指定值
        /// </summary>
        public virtual string Value { get; set; }

        /// <summary>
        /// 备注
        /// </summary>
        public virtual string Remark { get; set; }
    }

我们在调用的时候,让它限定为一个类型的ID进行精确查询,如下代码

//构建分页的条件和查询条件
var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
{
    DictType_ID = string.Concat(this.lblDictType.Tag)
};

这个精确或者模糊查询,则是在应用服务层里面定义规则的,这个之前没有详细介绍了,这里稍微补充说明一下。

在应用服务层接口类里面,重写CreateFilteredQuery可以设置GetAll的查询规则,重写ApplySorting则可以指定列表的排序顺序。

再次回到Winform界面的调用上来,删除类型下面字典数据的事件的处理函数如下所示。

        private async void menu_ClearData_Click(object sender, EventArgs e)
        {
            TreeNode selectedNode = this.treeView1.SelectedNode;
            if (selectedNode != null && selectedNode.Tag != null)
            {
                string typeId = selectedNode.Tag.ToString();
                var dict = await DictDataApiCaller.Instance.GetDictByTypeID(typeId);
                int count = dict.Count;

                var format = "您确定要删除节点:{0},该节点下面有【{1}】项数据";
                format = JsonLanguage.Default.GetString(format);
                string message = string.Format(format, selectedNode.Text, count);

                if (MessageDxUtil.ShowYesNoAndWarning(message) == DialogResult.Yes)
                {
                    try
                    {
                        await DictDataApiCaller.Instance.DeleteByTypeID(typeId);
                        await InitTreeView();
                        await BindData();
                    }
                    catch (Exception ex)
                    {
                        LogTextHelper.Error(ex);
                        MessageDxUtil.ShowError(ex.Message);
                    }
                }
            }
        }

我们看看编辑窗体界面的后台处理,编辑和更新数据的逻辑代码如下所示。

                #region 编辑大类
                var info = await DictTypeApiCaller.Instance.Get(new EntityDto<string>(ID));
                if (info != null)
                {
                    SetInfo(info);

                    try
                    {
                        var updatedDto = await DictTypeApiCaller.Instance.Update(info);
                        if (updatedDto != null)
                        {
                            MessageDxUtil.ShowTips("保存成功");
                            this.DialogResult = DialogResult.OK;
                        }
                    }
                    catch (Exception ex)
                    {
                        LogTextHelper.Error(ex);
                        MessageDxUtil.ShowError(ex.Message);
                    }
                } 
                #endregion

最后来一段gif动图,展示程序的操作功能吧。

好了,这些事件的使用规则一旦确定了,我们好利用代码生成工具对窗体界面的代码进行统一规则的生成,就好像我前面对于我Winform框架和混合框架里面的Winform窗体界面的生成一样,我们只需要稍微修改一下代码生成工具的NVelocity模板,利用上数据库表的元数据就可以快速生成整个框架所需要的代码了。

这样基于整个ABP框架,而快速应用起来的项目,其实开发项目的工作量看起来也不会很多,而且我们可以把字典、权限控制、整体框架等基础设施建设好,就会形成一整套的开发方法和思路了,这样对于我们利用ABP框架来开发业务系统,是不是有事半功倍的感觉。

一旦某个东西你很喜欢,你就会用的越来越好。

posted on 2020-10-30 14:11  springsnow  阅读(219)  评论(0编辑  收藏  举报

导航