2023年2月

目前微信的企业号已经切换到企业微信里面,这个是一个APP程序,提供了很丰富的企业应用,其中包括了业务审批处理,审批业务包括请假、报销、费用、出差等很多个审批场景,在Winform开发框架中工作流模块这些模块也是可以很好的实现,本篇随笔介绍如何参照企业微信审批业务,结合框架中的工作流模块,实现多种表单的审批处理。

1、企业微信审批

在开始介绍框架中工作流模块之前,我们先来了解下企业微信中的审批业务,如下界面所示,这些审批表单包括有:请假、报销、费用、出差、采购、加班、外出、用车、用章、合同审批、物品领用、付款、物品维修、会议室预定等这些常见的流程业务。

我们打开其中一个业务,看看其中的业务表单信息。

其中包括一些请假业务的相关信息,包括内容输入、附件管理等,这样就可以提交及抄送给对应给的人员了。

提交后,对业务表单的数据就可以查看及进行流程处理了,如下是业务表单的查看界面。

2、开发框架中工作流模块的业务审批

首先我们需要根据业务表单的数据,填写相关的信息,从而可以在业务的动态展示列表中展现可以新建的表单,如下界面所示。

至于每个业务表单的相关开发,我在随笔《
Winform开发框架中工作流模块的业务表单开发
》中介绍比较详细,每个流程业务表单主要涉及到几个窗体的处理,一个是新增编辑业务申请单、一个是查看业务申请和展示申请单的用户控件,以及一个常规的业务查询需要的界面,其中一些窗体我们已经进行了基类封装,简化了子类窗体的代码,如下界面所示。

例如对于请假业务申请单,我们的界面效果如下所示。

它主要是继承FrmAddApply ,并实现业务申请单数据校验、表单流程发送及数据显示(重新编辑的时候显示)等处理即可。界面代码相对比较简单了。

对于查看具体业务表单的显示界面,继承查看申请单的基类FrmViewApply类基类,那么这个子类只需要负责数据的展示就是了。

数据展示部分分为固定通用的申请单信息,以及业务表单信息两部分组成,其中附件处理则采用通用附件管理模块按钮即可,如下所示。

这些是在用户控件上进行展示的,设计状态下的用户控件效果如下所示。

上面的介绍的请假申请单是单表的情况,有时候,我们申请单是主从表的情况,如报销申请单,需要汇总总的报销信息、包括总金额,然后把各种明细记录提交,这种表单界面效果如下所示。

这种明细表单可以直接在表格控件Griview上进行新增、编辑处理。

查看详细的表单界面效果如下所示。

这种业务申请单比较单表而已,相对复杂一些,需要处理GridView的数据录入和保存处理,以及数据的显示处理,不过也是比较标准的处理,我们也是同样继承相同的基类,在子类实现不同的业务处理代码即可。

后面我们为了减轻开发工作流,把这些业务表单的界面使用代码生成工具一键生成,直接可用,这样就省事多了。

3、工作流业务界面的代码生成

从上面我们可以看到,其中对于工作流业务表单的窗体界面都可以实现标准的处理了,继承自某个基类,然后整合相关的数据处理规则即可。

那么我们提炼业务信息后,可以使用代码生成工具快速生成,这样可以极大提高我们的开发效率。

针对上面的构想,我们花费了好几天的时间,创建了工作流界面的自动生成规则和反复校验,最终整合到代码生成工具中方便开发。

对于主从表表的界面,我们依旧也可以使用代码生成工具进行快速的工作流界面生成。

至于如何使用这个功能,后面在开一篇随笔详细进行介绍过程。

WInform开发框架之工作流系列文章:

Winform开发框架之简易工作流设计


Winform开发框架中工作流模块的表设计分析


Winform开发框架中工作流模块的业务表单开发

Winform开发框架中工作流模块之审批会签操作

Winform开发框架中工作流模块之审批会签操作(2)


参照企业微信审批业务,在Winform开发框架中工作流模块的实现业务审批

在我们开发工作流模块的时候,有时候填写申请单过程中,暂时不想提交审批,那么可以暂存为草稿,以供下次继续填写或者提交处理,那么这个草稿的功能是比较实用的,否则对于一些填写内容比较多的申请单,每次要重填写很多数据,那会被用户骂的,从用户的角度上来讲,提供草稿保存的功能是比较友好的。本篇随笔介绍在工作流模块中使用一种通用的存储方式来存储及显示申请单草稿的信息。

1、申请单填写

在我们提交申请前,我们一般是需要填写一些相关的资料,如下界面所示。

这个表单记录的信息不多,不过提供存为草稿的功能也是要的,我们所有申请单都提供这个标准功能。

或者复杂一点的申请单

以往做过草稿保存,把记录复制在正式的申请单里面,设置它为草稿状态即可,这种方式可以实现,不过不好统一处理,本篇随笔介绍的是所有草稿存储在一个表里面,我们定义一些字段用来存储对应信息的JSON数据,然后需要的时候,把它们逐一解析为对应的对象即可,这种我们可以在基类窗体里面封装它的【存为草稿】的逻辑处理了。

2、草稿功能实现

首先我们定义一个存储草稿信息,可以对单表,也可以对主从表的数据,我们把它存储为对应的JSON字段即可,设计草稿的数据表如下所示。

在申请单填写的基类窗体里面,我们定义界面如下所示。

然后我们在基类提供一个通用的业务草稿保存处理函数,供子类进行调用即可。

        /// <summary>
        ///保存草稿/// </summary>
        /// <param name="title">草稿标题</param>
        /// <param name="mainJson">主业务表单数据</param>
        /// <param name="detailJson">从表业务表单数据(如无则为null)</param>
        /// <param name="detailJson2">从表业务表单数据(如无则为null)</param>
        /// <param name="detailJson3">从表业务表单数据(如无则为null)</param>
        /// <returns></returns>
        protected virtual void SaveDraft(string title, string mainJson, string detailJson = null,string detailJson2 = null, string detailJson3 = null)
{
var formInfo = BLLFactory<BLL.Form>.Instance.FindByID(this.FormID);
ArgumentCheck.Begin().NotNull(formInfo,
"表单对象");var infoDraft = newApplyDraftInfo();if(!string.IsNullOrEmpty(this.DraftId))
{
infoDraft.ID
= this.DraftId;//如果已有的则更新 }
infoDraft.BizDraftJson
=mainJson;
infoDraft.BizDraftJson2
=detailJson;
infoDraft.BizDraftJson3
=detailJson2;
infoDraft.BizDraftJson4
=detailJson3;
infoDraft.Form_ID
= this.FormID;
infoDraft.FormName
=formInfo.FormName;
infoDraft.Category
=formInfo.Category;
infoDraft.Title
=title;
infoDraft.Creator
=LoginUserInfo.ID;
infoDraft.CreateTime
=DateTime.Now;var flag = BLLFactory<ApplyDraft>.Instance.InsertUpdate(infoDraft, infoDraft.ID);

MessageDxUtil.ShowTips(
"保存草稿-" + (flag ? "成功" : "失败"));if(flag)
{
this.DialogResult =System.Windows.Forms.DialogResult.OK;
}
}

我们可以看到,草稿可以新增或者更新,如果对于已经存在的草稿,我们再次编辑的时候,不会新增另外一条记录,而是修改原来的记录。

对于普通单表的申请单处理,如下界面所示。

那么它的保存草稿的功能代码是如何实现的?

        /// <summary>
        ///保存草稿处理/// </summary>
        private void btnSaveDraft_Click(objectsender, EventArgs e)
{
string title = string.Format("{0}的付款申请单【{1}】(草稿)", LoginUserInfo.FullName, DateTime.Now.ToShortDateString());var info = tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
= base.LoginUserInfo.ID;
info.CreateTime
=DateTime.Now;//保存草稿:对象信息转换为JSON进行保存 SaveDraft(title, info.ToJson());
}

这里保存实际上就是获取对应的表单信息转换为JSON存储即可。

例如对于费用及费用明细的报销处理界面,如下所示。

那么我们的草稿处理有什么不同呢?

在填写申请单的子类我们实现按钮【存为草稿】的单击事件处理,代码如下所示。

        /// <summary>
        ///保存申请单草稿的处理/// </summary>
        private void btnSaveDraft_Click(objectsender, EventArgs e)
{
string title = string.Format("{0}的{1}报销申请单【{2}】(草稿)", LoginUserInfo.FullName, this.txtCategory.Text, DateTime.Now.ToShortDateString());var info = tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
= base.LoginUserInfo.ID;
info.CreateTime
=DateTime.Now;//获取费用明细 var list =GetDetailList();//保存草稿处理:如果有多个明细,可以增加在后面 SaveDraft(title, info.ToJson(), list.ToJson());
}

我们这里需要把费用信息、明细信息的对象转换为JSON对象,然后统一调用基类的保存草稿函数即可。

而对于草稿信息加载,还原为实际表单的信息显示,我们处理代码就是先解析JSON对象,转换为实际表单对象,然后进行界面赋值展示即可,如下代码所示。

完成这些,我们就可以在实际申请单业务中进行草稿的存储和显示了。

3、界面代码生成

以上代码相对都比较简单,不过我们为了开发工作流模块更加高效,统一使用代码生成工具Database2Sharp进行界面的代码生成即可,同时保存草稿、附件处理等代码都一并生成,直接使用即可。

对于主从表表的界面,我们依旧也可以使用代码生成工具进行快速的工作流界面生成。

至于如何使用这个功能,后面在开一篇随笔详细进行介绍过程。

WInform开发框架之工作流系列文章:

Winform开发框架之简易工作流设计


Winform开发框架中工作流模块的表设计分析


Winform开发框架中工作流模块的业务表单开发

Winform开发框架中工作流模块之审批会签操作

Winform开发框架中工作流模块之审批会签操作(2)


参照企业微信审批业务,在Winform开发框架中工作流模块的实现业务审批


Winform开发框架中工作流模块之申请单草稿处理

在Winform开发中,我们往往除了常规的单表信息录入外,有时候设计到多个主从表的数据显示、编辑等界面,单表的信息一般就是控件和对象实体一一对应,然后调用API保存即可,主从表就需要另外特殊处理,本随笔介绍如何快速实现主从表编辑界面的处理,结合GridControl控件的GridView控件对象,实现数据在列表中的实时编辑,非常方便。

1、主从表的界面设计及展示

主从表一般涉及两个以上的表,一个是主表,其他的是从表的,在实际情况下,一般包含两个表较多,我们这里以两个表的主从表关系进行分析处理。

例如我们建立两个报销申请单表关系如下所示。

对于报销的主从表信息,我们可以在列表中进行展示,如下界面所示,分为两部分:一部分是主表信息,一部分是从表信息,单击主表信息后,显示对应从表的列表信息。

那么我们新增一条主表记录的时候,那么可以弹出一个新的界面进行数据的维护处理,方便我们录入主从表的信息,界面如下所示。

上面界面包括了主表信息,以及从表的信息(在GridView中实时录入)两部分,这样填写后统一进行提交处理。

2、主从表编辑界面的处理

这里主要介绍一下主从表的编辑界面处理,也就是上面这个界面的实现处理。

其中初始化GridView的代码如下所示。

        /// <summary>
        ///初始化明细表的GridView数据显示/// </summary>
        private voidInitDetailGrid()
{
//初始清空列 this.gridView1.Columns.Clear();//设置部分列隐藏 this.gridView1.CreateColumn("ID", "编号").Visible = false;this.gridView1.CreateColumn("Header_ID", "主表编号").Visible = false;this.gridView1.CreateColumn("Apply_ID", "申请单编号").Visible = false;//添加下拉列表列,并绑定数据源 this.gridView1.CreateColumn("FeeType", "费用类型", 100).CreateComboBox().BindDictItems("费用类型");//创建日期列并指定格式 var OccurTime = this.gridView1.CreateColumn("OccurTime", "发生时间", 120).CreateDateEdit();
OccurTime.EditMask
= "yyyy-MM-dd HH:mm";
OccurTime.DisplayFormat.FormatString
= "yyyy-MM-dd HH:mm";//创建数值列 this.gridView1.CreateColumn("FeeAmount", "费用金额").CreateSpinEdit();//创建备注列 this.gridView1.CreateColumn("FeeDescription", "费用说明", 200).CreateMemoEdit();//初始化GridView,可以新增列 this.gridView1.InitGridView(GridType.NewItem, false, EditorShowMode.MouseDownFocused, "");//转义列内容显示 this.gridView1.CustomColumnDisplayText += newCustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);//处理单元格的样式 this.gridView1.RowCellStyle += newRowCellStyleEventHandler(gridView1_RowCellStyle);//不允许头部排序 this.gridView1.OptionsCustomization.AllowSort = false;//绘制序号 this.gridView1.CustomDrawRowIndicator += (s, e) =>{if (e.Info.IsRowIndicator && e.RowHandle >= 0)
{
e.Info.DisplayText
= (e.RowHandle + 1).ToString();
}
};
//对输入单元格进行非空校验 this.gridView1.ValidateRow += delegate(objectsender, ValidateRowEventArgs e)
{
var result = gridControl1.ValidateRowNull(e, new string[]
{
"FeeType"});
};
//新增行的内容初始化 this.gridView1.InitNewRow += (s, e) =>{
gridView1.SetRowCellValue(e.RowHandle,
"ID", Guid.NewGuid().ToString());
gridView1.SetRowCellValue(e.RowHandle,
"Header_ID", tempInfo.ID);
gridView1.SetRowCellValue(e.RowHandle,
"Apply_ID", tempInfo.Apply_ID);
gridView1.SetRowCellValue(e.RowHandle,
"OccurTime", DateTime.Now);
};
}
void gridView1_RowCellStyle(objectsender, DevExpress.XtraGrid.Views.Grid.RowCellStyleEventArgs e)
{
GridView gridView
= this.gridView1;if (e.Column.FieldName == "FeeAmount")
{
e.Appearance.BackColor
=Color.Green;
e.Appearance.BackColor2
=Color.LightCyan;
}
}
void gridView1_CustomColumnDisplayText(objectsender, DevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventArgs e)
{
string columnName =e.Column.FieldName;if (e.Column.ColumnType == typeof(DateTime))
{
if (e.Value != null)
{
if (e.Value == DBNull.Value || Convert.ToDateTime(e.Value) <= Convert.ToDateTime("1900-1-1"))
{
e.DisplayText
= "";
}
else{
e.DisplayText
= Convert.ToDateTime(e.Value).ToString("yyyy-MM-dd HH:mm");//yyyy-MM-dd }
}
}
}

上面代码都有详细的备注,主要就是我们根据数据库表的关系,创建对应显示的字段即可,其中有需要隐藏的那么就不要显示(方便获取对应的值)

            //设置部分列隐藏
            this.gridView1.CreateColumn("ID", "编号").Visible = false;this.gridView1.CreateColumn("Header_ID", "主表编号").Visible = false;this.gridView1.CreateColumn("Apply_ID", "申请单编号").Visible = false;

如果需要绑定下拉列表类似的字段,那么创建对应的数据类型,然后调用绑定函数绑定即可,如下面代码

            //添加下拉列表列,并绑定数据源
            this.gridView1.CreateColumn("FeeType", "费用类型", 100).CreateComboBox().BindDictItems("费用类型");

如果是一些特殊的输入需要设置格式显示或者掩码,那么如下所示

            //创建日期列并指定格式
            var OccurTime = this.gridView1.CreateColumn("OccurTime", "发生时间", 120).CreateDateEdit();
OccurTime.EditMask
= "yyyy-MM-dd HH:mm";
OccurTime.DisplayFormat.FormatString
= "yyyy-MM-dd HH:mm";

另外有一个值得注意的就是我们新增一行从表记录的时候,需要记录一些主表的属性,这样的话,我们就是在行初始化的时候,赋值给从表的隐藏列即可。

            //新增行的内容初始化
            this.gridView1.InitNewRow += (s, e) =>{
gridView1.SetRowCellValue(e.RowHandle,
"ID", Guid.NewGuid().ToString());
gridView1.SetRowCellValue(e.RowHandle,
"Header_ID", tempInfo.ID);
gridView1.SetRowCellValue(e.RowHandle,
"Apply_ID", tempInfo.Apply_ID);
gridView1.SetRowCellValue(e.RowHandle,
"OccurTime", DateTime.Now);
};

在界面中如果我们需要显示主表的信息,那么就根据条件获取对应的主表记录对象,然后显示给界面控件即可。

        /// <summary>
        ///显示常规的对象内容/// </summary>
        /// <param name="info"></param>
        private voidDisplayInfo(ReimbursementInfo info)
{
tempInfo
= info;//重新给临时对象赋值,使之指向存在的记录对象 txtCategory.Text=info.Category;
txtReason.Text
=info.Reason;
txtTotalAmount.Value
=info.TotalAmount;
txtNote.Text
=info.Note;
}

而保存的时候,我们把界面内容重新赋值给对应的主表对象。

        /// <summary>
        ///编辑或者保存状态下取值函数/// </summary>
        /// <param name="info"></param>
        private voidSetInfo(ReimbursementInfo info)
{
info.Category
=txtCategory.Text;
info.Reason
=txtReason.Text;
info.TotalAmount
=txtTotalAmount.Value;
info.Note
=txtNote.Text;

info.ApplyDate
=DateTime.Now;
info.ApplyDept
= base.LoginUserInfo.DeptId;
info.CurrentLoginUserId
= base.LoginUserInfo.ID;
}

而我们需要获取GridView明细输入的时候,就通过一个函数遍历获取GridView的行记录,转换为相应的对象即可,如下所示。

        /// <summary>
        ///获取明细列表/// </summary>
        /// <returns></returns>
        private List<ReimbursementDetailInfo>GetDetailList()
{
var list = new List<ReimbursementDetailInfo>();for (int i = 0; i < this.gridView1.RowCount; i++)
{
var detailInfo = gridView1.GetRow(i) asReimbursementDetailInfo;if (detailInfo != null)
{
list.Add(detailInfo);
}
}
returnlist;
}

这样处理完这些信息后,我们就可以在主表保存的时候,同时保存明细表信息即可。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public override boolSaveAddNew()
{
ReimbursementInfo info
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
= base.LoginUserInfo.ID;
info.CreateTime
=DateTime.Now;try{#region 新增数据 bool succeed = BLLFactory<Reimbursement>.Instance.Insert(info);if(succeed)
{
//可添加其他关联操作 var list =GetDetailList();foreach(var detailInfo inlist)
{
BLLFactory
<ReimbursementDetail>.Instance.InsertUpdate(detailInfo, detailInfo.ID);
}
return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}

其中代码

BLLFactory<ReimbursementDetail>.Instance.InsertUpdate(detailInfo, detailInfo.ID);

可以对新增记录保存,也可以对存在的记录进行更新。

通过上面的介绍,我们可以看到不同的主从表其实逻辑还是很通用的,我们可以把它们的逻辑抽取出来,通过代码生成工具进行快速生成即可。

在我们一般的应用系统里面,由于系统是面向不同类型的用户,我们所看到的菜单会越来越多,多一点的甚至上百个,但是我们实际工作接触的菜单可能就是那么几个,那么对于这种庞大的菜单体系,寻找起来非常不便。因此对菜单的个性化配置就显得尤为重要,本篇随笔就是基于这样的理念,提供用户对可见菜单进行一个动态配置,只选自己喜欢、常用的菜单显示出来即可,菜单的配置存储在数据库里面,在不同的客户端体验都是一样。本篇随笔主要介绍实现这样的功能的一个完整思路,部分代码逻辑可供参考。

1、 菜单列表的动态个性化配置的过程

在我们有些软件里面,我们可能在界面上顶部放置菜单,也可能在界面的左侧放置树形列表菜单,这种情况都有可能,本篇摘取其中之一,左侧菜单进行一个介绍菜单的配置处理。

例如我们在左侧根据用户权限展示相关的菜单信息,动态生成整个列表展示,大致的界面效果如下所示。

然后在功能列表上提供一个右键的菜单进行菜单的刷新、配置管理,如下界面所示。

通过配置功能,我们让用户进入一个配置管理界面,在其中配置显示自己感兴趣的菜单,然后进行保存即可,保存后同时刷新界面的功能菜单显示。

以上几个界面效果就是为了介绍整个菜单配置管理的一般过程,之所以把界面效果放在前面介绍,就是能够让我们有一个类似原型设计方式的感性认识,了解了相关的处理过程,我们就可以着手通过编码的方式来实现这个处理逻辑了。

2、菜单动态个性化配置的功能实现

上面介绍了大概的界面效果,有了参考,我们可以把它的实现思路通过代码实现出来。

1)参数的数据存储

首先我们需要了解,用户配置可以通过XML保存在本地,也可以通过数据库存储保存在服务器,后者在分布式的客户端的时候,可以处处一样,这样就不会造成体验上的差异,因此我们这里采用存储在数据库的方案。

这个存储我们沿用我之前介绍过的配置管理组件(SettingsProvider.net),我在随笔《
Winform开发框架之参数配置管理功能实现-基于SettingsProvider.net的构建
》中对它的使用进行了详细的介绍。

这个配置管理组件SettingsProvider.net使用起来也是比较方便的,可以选择存储在本地的对象,也可以选择存储在数据库的存储对象。

首先我们先定义一个存储的参数类,这个是使用这个组件所必须的存储对象信息,如下代码所示。

    /// <summary>
    ///用来控制人员管理显示菜单的参数配置/// </summary>
    public classUserMenuParameter
{
[DefaultValue(
"")]
[Description(
"用户ID")]public string UserID { get; set; }


[Description(
"用户设置可见的菜单")]public Dictionary<string, bool> VisibleDict { get; set; }
}

需要获取或存储这个对象信息的时候,我们初始化几个管理类,如下代码所示。

        //参数存储所需的相关对象
        privateSettingsProvider settings;privateISettingsStorage store;private UserMenuParameter parameter;

然后在配置管理界面窗体里面,初始化这几个对象,如下代码所示。

                //PortableStorage: 在运行程序目录创建一个setting的文件记录参数数据//DatabaseStorage:在数据库TB_UserParameter表存储用户配置参数
                store = newDatabaseStorage(LoginUserInfo.ID);
settings
= newSettingsProvider(store);
parameter
= settings.GetSettings<UserMenuParameter>();

这样我们就可以根据用户的ID,获取对应记录的信息并转换为相关的对象了,如果我们需要把修改的信息写会到存储介质里面,代码如下所示。

            try{
parameter
= settings.GetSettings<UserMenuParameter>();
parameter.VisibleDict
=dict;
parameter.UserID
=LoginUserInfo.ID;
settings.SaveSettings
<UserMenuParameter>(parameter);

ProcessDataSaved(sender, e);
//触发外部事件 this.DialogResult =System.Windows.Forms.DialogResult.OK;
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
return;
}

2)配置管理界面的实现

解决了参数的获取及存储功能后,我们需要编写一个界面来管理用户的菜单配置,也就是我们前面介绍的菜单配置管理界面。

我们这个界面的定义代码如下所示。

其中参数的数据存储就是应用了前面介绍的代码,这里需要根据用户的配置项初始化树形菜单的显示处理,通过InitTree的函数实现菜单的显示。

在显示菜单前,我们先介绍一下功能菜单显示的规则,仅当参数存在对应记录,并且该记录显式设置不可见,菜单才不可见,否则默认菜单是可以看到的。

这样确保了,在参数没有配置前,所有的菜单对当前用户是可见的,只有用户设置为不不可见,该菜单才不显示为不可见。

        /// <summary>
        ///获取菜单是否可见。///仅当参数存在对应记录,并且该记录显式设置不可见,菜单才不可见,否则默认菜单是可以看到的。/// </summary>
        /// <param name="id">菜单ID</param>
        /// <returns></returns>
        private bool GetVisibleMenu(stringid)
{
bool result = true;if (parameter != null)
{
var dict =parameter.VisibleDict;if(dict != null &&dict.ContainsKey(id))
{
result
=dict[id];
}
}
returnresult;
}

显示菜单的相关处理逻辑,就是根据上面的判断,然后确定是否勾选记录,如下代码所示。

存储用户勾选的记录的时候,我们需要遍历整个树节点,判断勾选了那些选项,然后把它保存数据库即可。

        /// <summary>
        ///递归获取选中的树节点集合/// </summary>
        /// <param name="node">树节点</param>
        /// <param name="dict">字典集合</param>
        /// <returns></returns>
        private Dictionary<string, bool> GetTreeSelection(TreeNode node, Dictionary<string, bool>dict)
{
if (node.Tag != null)
{
var check =node.Checked;var menuId = string.Concat(node.Tag);if(!dict.ContainsKey(menuId))
{
dict.Add(menuId, check);
}
}
foreach (TreeNode child innode.Nodes)
{
GetTreeSelection(child, dict);
}
returndict;
}

参数的保存操作如下所示。

        /// <summary>
        ///保存用户配置信息/// </summary>
        private void btnOK_Click(objectsender, EventArgs e)
{
//获取用户勾选的树列表,存放在字典集合里面 var dict = new Dictionary<string, bool>();foreach(TreeNode node in this.treeView1.Nodes)
{
GetTreeSelection(node, dict);
}
try{//重新获取参数信息,并设置新值后保存 parameter = settings.GetSettings<UserMenuParameter>();
parameter.VisibleDict
=dict;
parameter.UserID
=LoginUserInfo.ID;
settings.SaveSettings
<UserMenuParameter>(parameter);

ProcessDataSaved(sender, e);
//触发外部事件 this.DialogResult =System.Windows.Forms.DialogResult.OK;
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
return;
}
}

3)主界面的相关处理

以上处理完成后,我们在主界面的工具栏右键菜单添加一个菜单项,用来进入配置界面的,如下逻辑代码所示。

        private void tool_MenuSetting_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
MenuSetting();
}
/// <summary> ///配置菜单项/// </summary> private voidMenuSetting()
{
FrmMenuSetting dlg
= newFrmMenuSetting();
dlg.OnDataSaved
+= (s, arg) =>{//用户保存参数后,提示用户更新树形列表 InitToolbar();
};
dlg.ShowDialog();
}

这样界面配置参数并保存后,界面的树形菜单会及时得到更新处理。

另外,我们主界面的树形列表,也要根据配置参数的信息作相关的调整,如果用户配置了不显示某个菜单,那么主界面也要根据配置参数控制显示。

3、总结

以上就是整个菜单列表的动态个性化配置管理的整体思路和实现步骤代码,主要的界面考量还是以用户的视觉来考虑界面的布局和功能,如果在几百个菜单项中寻找几个常用的菜单,每次是一个比较耗时无聊的操作,因此提供一个个性化的界面,根据工作情况的不同,显示一些和自己相关的功能即可。

例如有些情况下,我们的菜单显示,希望通过工具栏的方式进行控制显示,如下界面效果所示。

那么配置维护界面还是差不多,只是我们控制工具栏的显示逻辑有所不同而已,对于RibbonPage及其功能菜单的动态生成处理如下所示。

本篇随笔主要还是希望读者借鉴配置存储和菜单个性化管理的思路,具体的逻辑会因用户界面的不同,使用的控件不同而有所差异,不过总体思路是一致的即可。

例如有些参数的配置管理,可以统一使用一个配置管理界面进行维护,如我之前的随笔介绍的界面功能一样。

希望本篇随笔对你有所启发,感谢耐心阅读。

在一个给客户做的项目中,界面要求修改增加通用工具栏按钮的事件处理,也就是在主界面中放置几个固定的功能操作按钮,打开不同的页面的时候,实现对应页面的功能处理,这种和我标准的界面处理方式有所不同,标准的列表界面,一般在界面中放置了一些常规的按钮,如查询/更新、新建、编辑、删除、导入、导出等常规操作,现在是需要把这些提升到主界面的层次上放置按钮,这种处理方式也算是提供一种方便吧。本篇随笔介绍实现这个功能的思路和代码实现逻辑。

1、界面功能介绍

这里要实现的通用工具栏按钮的事件处理,具体的界面是这样的,我们处于一个多文档的主界面中,在主界面的顶部工具栏放置这些通用的工具栏按钮,以便提高相关的处理便利和效率。

从上面的界面我们可以看到,程序主界面的顶部工具栏上有一些常规的操作,包括如查询、新建、编辑、删除、导入、导出等按钮,可以为程序的功能操作提供方便。

当我们打开一个新的窗体页面,那么对应的按钮事件也应该和这个窗体相关,或者我们切换到其他窗体,那么按钮的处理事件也应该跟着变化到对应的窗体上,这种就是我们需要的效果。

上面的逻辑也就是我们新建一个窗体、或者切换一个窗体,那么需要通知顶部工具栏进行更新或者进行一个对接的处理才可以。

2、界面窗体基类的处理

由于我们想尽可能的减少开发的工作量,因此希望在基类进行一些事件或者接口的封装,以便降低新建列表窗体的时候的代码编写。

对于一个如查询操作的处理事件,我们需要定义一个接口来实现这个功能,这样我们才可以在打开的窗体的时候,把它转换为对应的接口处理。

例如我们定义一个IMenuAction的接口,以更新事件为例,需要实现三部分的接口,一个是事件定义,一个是判断是否包含更新事件,一个是具体的处理逻辑。

    /// <summary>
    ///定义菜单中的通用按钮事件/// </summary>
    public interfaceIMenuAction
{
/// <summary> ///更新按钮事件/// </summary> eventEventHandler Refresh_MenuEvent;/// <summary> ///是否包含更新事件/// </summary> bool HasEventRefresh { get; }/// <summary> ///更新操作/// </summary> void ProcessRefresh(object sender, EventArgs e);

对于其他总共6个标准的按钮事件处理,我们也同样一样定义,那么整个IMenuAction的接口定义如下所示。

    /// <summary>
    ///定义菜单中的通用按钮事件/// </summary>
    public interfaceIMenuAction
{
/// <summary> ///更新按钮事件/// </summary> eventEventHandler Refresh_MenuEvent;/// <summary> ///新建按钮事件/// </summary> eventEventHandler Add_MenuEvent;/// <summary> ///编辑按钮事件/// </summary> eventEventHandler Edit_MenuEvent;/// <summary> ///删除按钮事件/// </summary> eventEventHandler Delete_MenuEvent;/// <summary> ///导入按钮事件/// </summary> eventEventHandler Import_MenuEvent;/// <summary> ///导出按钮事件/// </summary> eventEventHandler Export_MenuEvent;/// <summary> ///是否包含更新事件/// </summary> bool HasEventRefresh { get; }/// <summary> ///是否包含增加事件/// </summary> bool HasEventAdd { get; }/// <summary> ///是否包含编辑事件/// </summary> bool HasEventEdit { get; }/// <summary> ///是否包含删除事件/// </summary> bool HasEventDelete { get; }/// <summary> ///是否包含导入事件/// </summary> bool HasEventImport { get; }/// <summary> ///是否包含导出事件/// </summary> bool HasEventExport { get; }/// <summary> ///更新操作/// </summary> void ProcessRefresh(objectsender, EventArgs e);/// <summary> ///增加操作/// </summary> void ProcessAdd(objectsender, EventArgs e);/// <summary> ///编辑操作/// </summary> void ProcessEdit(objectsender, EventArgs e);/// <summary> ///删除操作/// </summary> void ProcessDelete(objectsender, EventArgs e);/// <summary> ///导入操作/// </summary> void ProcessImport(objectsender, EventArgs e);/// <summary> ///导出操作/// </summary> void ProcessExport(objectsender, EventArgs e);
}

那么对于列表界面的基类窗体,我们除了让他继承自XtraForm这个标准的窗体外,我们还让它实现对应的IMenuAction接口,如下是列表界面基类BaseDock的窗体定义。

    /// <summary>
    ///用于一般列表界面的基类/// </summary>
    public partial class BaseDock : XtraForm, IMenuAction

那么这个BaseDock的基类需要实现那些通用按钮事件的接口,如下所示。

        #region 通用按钮菜单事件
        /// <summary>
        ///更新按钮事件/// </summary>
        public eventEventHandler Refresh_MenuEvent;/// <summary>
        ///根据事件判断是否有/// </summary>
        public boolHasEventRefresh
{
get{return Refresh_MenuEvent != null;
}
}
/// <summary> ///调用事件定义/// </summary> public void ProcessRefresh(objectsender, EventArgs e)
{
if (Refresh_MenuEvent != null)
{
Refresh_MenuEvent(sender, e);
}
}

.......................
#endregion

通过基类的这样处理,我们就可以在具体列表窗体里,赋值给对应的事件就可以了。

3、列表窗体界面的处理

例如我们处理一个应用菜单的列表管理界面,定义窗体如下所示,继承了我们所实现IMenuAction的基类BaseDock。

    /// <summary>
    ///应用菜单管理/// </summary>
    public partial class FrmApplicationMenu : BaseDock

那么我们需要指定它的几个处理事件,初始化代码如下所示。

        /// <summary>
        ///初始化公用菜单按钮的处理/// </summary>
        private voidInitMenuAction()
{
this.Refresh_MenuEvent += (s, e) =>{ btnSearch_Click(s, e); };this.Add_MenuEvent += (s, e) =>{ btnAddNew_Click(s, e); };this.Edit_MenuEvent += (s, e) =>{ winGridViewPager1_OnEditSelected(s, e); };this.Delete_MenuEvent += (s, e) =>{ winGridViewPager1_OnDeleteSelected(s, e); };this.Import_MenuEvent += (s, e) =>{ btnImport_Click(s, e); };this.Export_MenuEvent += (s, e) =>{ btnExport_Click(s,e);};
}

这样我们就知道对应接口是如何和具体的页面事件发生关系了。

4、主窗体界面的处理

我们在构建主界面的左侧功能树的时候,我们通过树列表的选中事件加载对应的窗体,具体代码如下所示。

    //处理树形菜单的点击操作,如果TAG存在,则解析并加载对应的页面到多文档里面
    treeView.AfterSelect += (sender, e) =>{string tag = e.Node.Tag as string;if (!string.IsNullOrEmpty(tag))
{
LoadPlugInForm(tag);
}
};

在这个函数里面,我们最终是通过配置动态构建对应的窗体,LoadPlugInForm逻辑的最终实现的部分代码如下所示。

    var form = LoadMdiForm(this.mainForm, objType, isShowDialog);
RefreshButton(form);

这里我们构建窗体或者激活窗体都获得一个窗体对象,然后刷新功能按钮的状态处理即可。

    /// <summary>
    ///更新按钮状态/// </summary>
    /// <param name="form">当前窗体</param>
    public voidRefreshButton(Form form)
{
this.CurrentForm =form;
IMenuAction action
= form asIMenuAction;if (action != null)
{
//事件处理后再判断 menuButton.refresh.Enabled =(action.HasEventRefresh);
menuButton.add.Enabled
=(action.HasEventAdd);
menuButton.edit.Enabled
=(action.HasEventEdit);
menuButton.delete.Enabled
=(action.HasEventDelete);
menuButton.import.Enabled
=(action.HasEventImport);
menuButton.export.Enabled
=(action.HasEventExport);
}
}

这样的处理,就可以实现了在主界面的多文档处理中,不管我们是新建一个列表窗体,还是激活切换到一个其他的窗体的时候,按钮的状态和对应的窗体绑定,实现随时更新显示。

当然通用工具栏按钮的事件我们需要初始化,它是根据当前选中的窗体进行统一的处理的,具体代码如下所示。

        /// <summary>
        ///初始化菜单按钮的事件处理/// </summary>
        private voidInitMenuEvent()
{
//事件绑定//先实现事件绑定 menuButton.add.ItemClick += (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessAdd(s, e);
}
}
};
menuButton.edit.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessEdit(s, e);
}
}
};
menuButton.delete.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessDelete(s, e);
}
}
};
menuButton.import.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessImport(s, e);
}
}
};
menuButton.export.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessExport(s, e);
}
}
};
menuButton.refresh.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessRefresh(s, e);
}
}
};
}

上面这个事件只需要绑定一次,不会造成按钮的事件多次绑定问题,虽然一次绑定,它的具体处理还是和当前窗体有关,它会把当前窗体转换为对应的IMenuAction接口,然后调用对应的处理函数实现功能的绑定。

以上就是我对于实现这种通用按钮事件处理的思路和具体逻辑代码,供大家参考学习,有错漏之处还请多多包涵。