2023年2月

我在上一篇《
Winform开发框架之通用人员信息管理
》随笔中介绍了这个通用人员信息管理的大致实现界面和思路,本篇就其中的实现细节做进一步的分析和共享,希望大家对其中的实现代码进行一个了解,并希望多多提出宝贵意见。通用人员信息管理模块,这个模块其实在很多场合都可能用到,如企业员工管理、科室员工管理等等,这些要求登记人员详细资料及图片等信息的系统模块。

1、项目框架布局

以上几个模块分开是为了适应更多的项目需要,如可能用到WCF模块,那么实体类需要独立引用。但是如果是纯粹的Winform模块,以最少化项目为管理原则,那么就不需要那么多工程了,这样也方便项目的集成引用。虽然我们把基于多数据库支持的逻辑及数据库访问实现,封装到一个WHC.StaffData.Core项目里面,但是如果是Winform的项目应用,我们只需要引用他的代码路径就可以了,这样就保证了一份代码实体,多次外部应用的良好处理。如下所示。

这样的Winform模块整合后,我们在实际集成项目的时候,就很方便,人员管理模块就只有一个WHC.StaffDataDx程序集,不用管理太多关于人员模块的程序DLL了(否则有UI层、BLL层、DAL层、IDAL层、Entity层等等很多项目工程,这样一旦模块分的比较多,几十个就出来了,非常不便于管理)。

以上就是一个集成测试项目用到的相关应用,我们看到,人员管理模块里面只有一个程序集,其他的是人员管理模块用到的其他公用模块,这样比较方便管理,更利于大型项目的集成工作,不同的模块职责不同,维护可能也交由不同的技术人员维护即可,而且最大的优点是所有项目都通用,提高重复利用的效率。

集成测试项目是我们实际集成的参考例子,使用很简单,如下所示。

private void btnAddStaff_Click(objectsender, EventArgs e)
{
FrmEditStaff dlg
= newFrmEditStaff();
dlg.ShowDialog();
}
private void btnStaffList_Click(objectsender, EventArgs e)
{
FrmStaff dlg
= newFrmStaff();
dlg.ShowDialog();
}

2、多数据库支持

整个小项目,是我Winform开发框架的缩影,我的Winform开发框架都基本上遵循一个规则来做各独立模块的开发,都具有多数据库支持的特性,良好的框架布局和高质量的代码特性。

我的传统Winform开发框架的设计图,如下所示。其中界面层UI直接访问BLL层,不需要通过网络,其中公用辅助类库Common层、实体类层可以在各个层中访问,并把常用的权限管理、字典管理封装为组件模块,直接调用,底层则使用工厂方式,来支持各种不同的数据库,其中UI层、BLL层、DAL层、实体层均使用继承类方式实现最良好的封装、最优的代码设计。

当然下面的框架示意图也是适用于这个人员管理模块的了。

实现多数据库支持的框架项目实际代码结构如下所示。

他们几个层之间的关系我在很早介绍框架数据库结构就介绍过,如下所示。

他们几个层之间的关系,具有非常强的继承关系,数据访问层有一个超级基类,抽象于各种数据库的基础上而形成,对于不同的数据库,还有一个一般的基类,用来实现详细化的数据库特性,每个业务类,明确继承关系后,就具有一份非凡的本领(具有几乎所有常用到的各类通用接口),很多时候我们基本不需要写任何和数据库操作的代码了。

3、界面实现

对于界面的设计,一直也希望能比较好的体现出我的设计思想,在整个人员信息管理中,有人员学习情况、履历情况、家庭情况、出国情况、职称情况等,界面基本上比较统一,就是一个列表的管理,常规的管理思路,一般还需要对列表的顺序可以自由调整,这是很常见的功能,因此,这里也引入了一个可以调整顺序的
GridControlDrager
辅助类,这个就是在使用时,用户可以拖动记录到任意的顺序。

实现代码如下所示:

        private void StaffResumeControl_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
GridControlDrager drager
= new GridControlDrager(this.gridControl1);
drager.ProcessDragRow
+= newGridControlDrager.ProcessEventHandler(drager_ProcessDragRow);
}
}
bool drager_ProcessDragRow(int sourceRowHandle, inttargetRowHandle)
{
DataTable table
= this.gridControl1.DataSource asDataTable;if (table != null)
{
string sourceID = this.gridView1.GetRowCellValue(sourceRowHandle, "ID").ToString();string targetID = this.gridView1.GetRowCellValue(targetRowHandle, "ID").ToString();bool result = BLLFactory<StaffResume>.Instance.UpdateTwoSeq(sourceID, targetID);if(result)
{
DataRow sourceRow
=table.Rows[sourceRowHandle];

DataRow row
=table.NewRow();
row.ItemArray
=sourceRow.ItemArray;

table.Rows.Remove(sourceRow);
table.Rows.InsertAt(row, targetRowHandle);
this.gridControl1.DataSource =table;this.gridView1.FocusedRowHandle =targetRowHandle;
}
}
return true;
}

几个模块大致的设计界面如下所示,然后在窗体里面集成就可以了。

运行时刻的界面效果如下所示。

以上还包含了附件的管理,这个模块在之前的

Winform开发框架之通用附件管理模块
》有介绍,这里不再赘述了。

4、Word报表导出操作

上篇随笔介绍了Word导出的格式,导出的人员信息表如下所示:


我们看到,里面很多信息是单字段的,有部分是列表的,对于单字段,我们采用在模板中添加标签引用方式,然后替换其中的标签引用的文本实现,如下所示。

#region 通过书签方式替换内容Dictionary<string, string> dictBookMark = new Dictionary<string, string>();//姓名,性别,出生时间,政治面貌,党团时间,民族,籍贯,职务,任职时间,工作时间,最高学历,获学历时间,最高学位,//获学位时间,婚否,职称,职称时间,是否独生子女
                StaffInfo staffInfo = BLLFactory<Staff>.Instance.FindByID(ID);if (staffInfo != null)
{
dictBookMark.Add(
"姓名", staffInfo.Name);
dictBookMark.Add(
"性别", staffInfo.Sex);
dictBookMark.Add(
"出生时间", staffInfo.BirthDate.GetDateTimeString("yyyy.MM.dd"));
dictBookMark.Add(
"政治面貌", staffInfo.Political);
dictBookMark.Add(
"党团时间", staffInfo.PartyDate.GetDateTimeString("yyyy.MM"));
dictBookMark.Add(
"民族", staffInfo.Nationality);
dictBookMark.Add(
"籍贯", staffInfo.NativePlace);
dictBookMark.Add(
"职务", staffInfo.OfficialRank);
dictBookMark.Add(
"任职时间", staffInfo.ServingDate.GetDateTimeString("yyyy.MM"));
dictBookMark.Add(
"工作时间", staffInfo.WorkingDate.GetDateTimeString("yyyy.MM"));
dictBookMark.Add(
"最高学历", staffInfo.HighestEducation);
dictBookMark.Add(
"获学历时间", staffInfo.EducationDate.GetDateTimeString("yyyy.MM"));
dictBookMark.Add(
"最高学位", staffInfo.HighestDegree);
dictBookMark.Add(
"获学位时间", staffInfo.DegreeDate.GetDateTimeString("yyyy.MM"));
dictBookMark.Add(
"婚否", staffInfo.MarriageStatus);
dictBookMark.Add(
"职称", staffInfo.Titles);
dictBookMark.Add(
"职称时间", staffInfo.TitlesDate.GetDateTimeString("yyyy.MM"));
dictBookMark.Add(
"是否独生子女", staffInfo.ChildStatus);

StaffAwardInfo awardInfo
= BLLFactory<StaffAward>.Instance.FindSingle(condition);if (awardInfo != null)
{
dictBookMark.Add(
"受奖情况", awardInfo.Note);
}
}

Aspose.Words.Document doc
= newAspose.Words.Document(templateFile);foreach (string name indictBookMark.Keys)
{
Aspose.Words.Bookmark bookmark
=doc.Range.Bookmarks[name];if (bookmark != null)
{
bookmark.Text
=dictBookMark[name];
}
}
#endregion

对于列表的内容,我们就要引入Aspose.Word的MailMerge功能了,先在固定模板中插入并定义好相关的域引用,如下所示。

创建一系列的域代码引用后,才能利用Aspose.Word的MailMerge功能。

上图红色部分为对于一个列表必须要创建的域代码,包括TableStart:和TableEnd的标识。

创建好这些后,绑定数据源的操作不算复杂,如下所示。

                Aspose.Words.DocumentBuilder builder = newAspose.Words.DocumentBuilder(doc);

List
<StaffStudyInfo> studyList = BLLFactory<StaffStudy>.Instance.Find(condition);
DataTable dtStudy
= DataTableHelper.ToDataTable<StaffStudyInfo>(studyList);
dtStudy.TableName
= "study";
FillStaticRow(dtStudy,
5);

List
<StaffTitlesInfo> titleList = BLLFactory<StaffTitles>.Instance.Find(condition);
DataTable dtTitle
= DataTableHelper.ToDataTable<StaffTitlesInfo>(titleList);
dtTitle.TableName
= "title";
FillStaticRow(dtTitle,
5);

.....................

                DataSet ds = newDataSet();
ds.Tables.Add(dtStudy);
ds.Tables.Add(dtTitle);
ds.Tables.Add(dtResume);
ds.Tables.Add(dtAbroad);
ds.Tables.Add(dtFamily);

doc.MailMerge.ExecuteWithRegions(ds);

doc.Save(saveFile);
if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
Process.Start(saveFile);
}

其中的FillStaticRow函数,我是用来生成固定怎么多行的操作,使得列表默认不少于固定的函数,否则列表不好看。

以上就是我在开发这个模块中的一些经验心得,希望抛砖引玉,对大家有帮助的同时,也能获得更多的反馈意见,相互促进交流。

因有一个业务需要在Winform界面中,以类似Excel表格界面中录入相关的数据(毕竟很多时候,客户想利用成熟的软件体验来输入他们想要的东西),其中界面需要录入基础信息,列表信息,图片信息等,综合这些就是例如下面这样的界面效果。本文主要针对如何利用FarPoint Spread表格控件实现类似Excel界面丰富数据的保存及显示,以及在使用过程中的一些经验心得,希望对大家在开发Winform的Excel数据录入和显示方面的开发有一定帮助。

从以上的界面分类可以看到,大致可以分为几个类型的数据,一个是基础字段数据,一个是有多行的列表数据,一个是图片数据,还有就是备注信息的显示录入了。下面我来分类介绍这些功能的实现。

1、类似Excel的列表总体界面设计

首先,这个列表需要在Winform的界面中进行设计,拖入一个Farpoint控件到Winform界面上,设置好布局等属性,然后在右键菜单上启动Spread Designer就可以设计相关的Excel样式表格的内容了。

注意,这里界面一般是在窗体中设计的,当然你的内容可以通过复制粘贴的方式,从Excel文档拷贝过来,效果看起来一样的,非常不错。不过,虽然Farpoint Spread控件提供了一个另存为Xml文件的操作,并且可以通过API,Open一个XML文件,不过Open的XML文件后,好像内容不能进行修改的,而且类型CellType也是Null的,所以如果要在一个窗体上动态加载布局好像做不到,至少我没有做到。不过对于开发来说,我们在设计时刻,设计好Excel样式的列表界面,也未尝不是一件好事。

2、下拉列表的绑定

在Excel列表中,我们很多时候,为了输入的方便,需要通过下拉列表方式输入内容,这样可以提高速度和用户体验,但这些内容必须是通过数据库内容进行绑定的,Farpoint Spread控件是如何做到绑定下拉列表的数据的呢。首先Farpoint Spread控件由很多输入的内容,其中就包括有ComoBox类型,如下所示。

我们在指定下拉的类型后,Excel列表的显示方式也跟着变化为下面样式了。

以上打勾的就是我们下一步需要绑定列表数据的列表了,绑定列表的数据也不麻烦,就是需要明确Cell的序号,绑定给他数据源就可以了,不过说实话,经常要数着Cell的行列号是什么数字,有点不方便。

        private voidBindDict()
{
FarPoint.Win.Spread.Cell cell;
//品名 cell = this.fpSpread1_Sheet1.Cells[1,9];
FarPoint.Win.Spread.CellType.ComboBoxCellType productType
= newFarPoint.Win.Spread.CellType.ComboBoxCellType();
productType.BindDictItems(
"品名");
cell.CellType
=productType;//客户名 cell = this.fpSpread1_Sheet1.Cells[4, 8];
FarPoint.Win.Spread.CellType.ComboBoxCellType customerType
= newFarPoint.Win.Spread.CellType.ComboBoxCellType();
customerType.BindDictItems(
"客户名");
cell.CellType
=customerType;//款号 cell = this.fpSpread1_Sheet1.Cells[1, 12];
FarPoint.Win.Spread.CellType.ComboBoxCellType styleType
= newFarPoint.Win.Spread.CellType.ComboBoxCellType();
styleType.BindDictItems(
"款号");
cell.CellType
=styleType;//面料 cell = this.fpSpread1_Sheet1.Cells[1, 15];
FarPoint.Win.Spread.CellType.ComboBoxCellType materialType
= newFarPoint.Win.Spread.CellType.ComboBoxCellType();
materialType.BindDictItems(
"面料");
cell.CellType
=materialType;

}

其中代码的BindDictItems我用了扩展方法,所以能通过对象直接调用,具体的函数代码如下所示,就是调用字典业务类获取数据,赋值给Items属性即可,注意其中的Edittable最好选择为true,否则它只是显示里面列表的内容,类似DropdownList那样。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        public static void BindDictItems(this FarPoint.Win.Spread.CellType.ComboBoxCellType combo, stringdictTypeName)
{
Dictionary
<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
List
<string> listData = new List<string>();foreach (string key indict.Keys)
{
listData.Add(key);
}
combo.Items
=listData.ToArray();
combo.Editable
= true;
}

3、如何构造界面自定义录入

为了输入方便,对于一些例如弹出框选择内容,图片编辑,备注内容(很长的时候)的编辑,这些一般来说,我们通过自定义界面来录入比较好,比较Excel样式的界面,录入单元格很小,也有时候实现不了的。所以通过制定控件单元格的单击事件,用来处理特殊录入信息的操作。

this.fpSpread1.CellClick += new FarPoint.Win.Spread.CellClickEventHandler(fpSpread1_CellClick);

展开界面部分给大家看看,就是很把内容

        void fpSpread1_CellClick(objectsender, FarPoint.Win.Spread.CellClickEventArgs e)
{
FarPoint.Win.Spread.Cell cell
= this.fpSpread1_Sheet1.Cells[e.Row, e.Column];
fpSpread1_Sheet1.SetActiveCell(e.Row, e.Column);
if(e.Column == 14 && e.Row == 6)
{
#region 图片操作FrmImageEdit dlg= newFrmImageEdit();if (!string.IsNullOrEmpty(ID))
{
dlg.ID
=ID;
dlg.IsNew
= false;
}
else{
dlg.ID
=NewID;
dlg.IsNew
= true;
}
dlg.OnDataSaved
+= newEventHandler(dlgPicture_OnDataSaved);
dlg.ShowDialog();
#endregion}else if (e.Column == 1 && e.Row == 42)
{
#region 注意事项 object value = this.fpSpread1_Sheet1.Cells[e.Row, e.Column].Value;if (value != null)
{
FrmEditNote dlg
= newFrmEditNote();
dlg.txtContent.Text
=value.ToString();if (dlg.ShowDialog() ==System.Windows.Forms.DialogResult.OK)
{
this.fpSpread1_Sheet1.Cells[e.Row, e.Column].Value =dlg.txtContent.Text;
}
}
#endregion}

例如,对于下拉列表内容,需要进行弹出式选择内容,如下界面所示。

对于图片单元格,单击就可以弹出下面的窗体,方便编辑或者查看。

对于备注内容,我们让他弹出一个窗体,更好展现和编辑。

4、数据的显示和保存

对于普通的主表数据字段的显示很简单,把内容赋值给对应的单元格Text属性即可,如下所示。

        /// <summary>
        ///数据显示的函数/// </summary>
        public voidDisplayData()
{
if (!string.IsNullOrEmpty(ID))
{
#region 显示信息CraftHeaderInfo info= BLLFactory<CraftHeader>.Instance.FindByID(ID);if (info != null)
{
this.fpSpread1_Sheet1.Cells[1, 9].Text =info.ProductName;this.fpSpread1_Sheet1.Cells[1, 12].Text =info.StyleNo;this.fpSpread1_Sheet1.Cells[1, 15].Text = info.Material;

保存的时候,把对应的内容保存到实体类进行数据保存操作即可。

        /// <summary>
        ///编辑或者保存状态下取值函数/// </summary>
        /// <param name="info"></param>
        private voidSetInfo(CraftHeaderInfo info)
{
info.ProductName
= this.fpSpread1_Sheet1.Cells[1, 9].Text;//品名 info.StyleNo = this.fpSpread1_Sheet1.Cells[1, 12].Text;//款号 info.Material = this.fpSpread1_Sheet1.Cells[1, 15].Text;//面料

更多数据的时候,我们把内容保存分开,各个函数负责不同的部分即可,在主表保存后继续保存其他部分的内容,例如红色部分就是其他部分的保存操作。

        private void btnSave_Click(objectsender, EventArgs e)
{
if (!string.IsNullOrEmpty(ID))
{
CraftHeaderInfo info
= BLLFactory<CraftHeader>.Instance.FindByID(ID);if (info != null)
{
SetInfo(info);
try{#region 更新数据 bool succeed = BLLFactory<CraftHeader>.Instance.Update(info, info.ID.ToString());if(succeed)
{
SaveProcess(info.ID);
SaveAccessories(info.ID);
SaveIndicateSize(info.ID);
SaveColorPair(info.ID);
//可添加其他关联操作 ProcessDataSaved(this.btnSave, newEventArgs());

MessageDxUtil.ShowTips(
"保存成功");
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}
else{

例如工艺过程是一个列表数据,保存的时候,需要指定行列的属性进行操作,而且我们添加一个Seq的序列号,用来保存内容的顺序,这样加载的时候,我们就按照这个循序进行加载显示,否则会出现问题。

        private void SaveProcess(stringheaderId)
{
string condition = string.Format("Header_ID = '{0}'", headerId);
List
<CraftProcessInfo> list = BLLFactory<CraftProcess>.Instance.Find(condition);//(e.Column == 1 && (e.Row >= 6 && e.Row < 30)) int i = 0;for (int row = 6; row < 30; row++)
{
CraftProcessInfo info
= GetProcess(i++, list);
info.Header_ID
=headerId;int col = 0;
info.HandNo
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Process
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Models
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.NeedleWork
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Flower
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.DownLine
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.PinCode
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.KnifeGate
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Note
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item1
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item2
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item3
= this.fpSpread1_Sheet1.Cells[row, col++].Text;

BLLFactory
<CraftProcess>.Instance.InsertUpdate(info, info.ID);
}
}

其中GetProcess函数,就是一个列表中查找对应顺序的内容,如果有,那么我们更新这个对应顺序的内容,如果没有,那么我们认为它是新的数据,这样就新增到数据库中,所以最后用了InserUpdate就是这个道理。其中GetProcess函数逻辑代码如下所示。

        private CraftProcessInfo GetProcess(int index, List<CraftProcessInfo>list)
{
CraftProcessInfo info
= newCraftProcessInfo();if (list.Count >index)
{
info
=list[index];
}
info.Seq
= index + 1;//重新调整顺序号 returninfo;
}

另外注意的时候,有些单元格是合并列的,所以一定要注意算好他的行列号哦。有些地方可能需要跳行。

        private void SaveAccessories(stringheaderId)
{
string condition = string.Format("Header_ID = '{0}'", headerId);
List
<AccessoriesInfo> list = BLLFactory<Accessories>.Instance.Find(condition);//(e.Column == 1 && (e.Row >= 31 && e.Row < 35)) int i = 0;for (int row = 31; row < 35; row++)
{
AccessoriesInfo info
= GetAccessories(i++, list);
info.Header_ID
=headerId;int col = 1;
info.Name
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Consumption
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Position
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
col
++;//空跳一列 info.Item1 = this.fpSpread1_Sheet1.Cells[row, col++].Text;
col
++;//空跳一列 col++;//空跳一列 info.Item2 = this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item3
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item4
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item5
= this.fpSpread1_Sheet1.Cells[row, col++].Text;
info.Item6
= this.fpSpread1_Sheet1.Cells[row, col++].Text;

BLLFactory
<Accessories>.Instance.InsertUpdate(info, info.ID);
}
}

5、Excel表格的数据打印及导出。

使用这个Farpoint Spread的空间,对于里面的内容进行打印或者导出Excel非常方便,代码也不多,如下所示。

        private void btnPrint_Click(objectsender, EventArgs e)
{
PrintInfo pi
= newPrintInfo();
pi.Header
= "成衣工艺单";
pi.JobName
= "成衣工艺单";
pi.Orientation
=PrintOrientation.Auto;
pi.PageOrder
=PrintPageOrder.Auto;
pi.ShowPrintDialog
= true;
pi.PrintNotes
=PrintNotes.AtEnd;for (int i = 0; i < this.fpSpread1.Sheets.Count; i++)
{
pi.ShowPrintDialog
= (i == 0);this.fpSpread1.Sheets[i].PrintInfo =pi;
fpSpread1.PrintSheet(i);
}
}
private void btnExport_Click(objectsender, EventArgs e)
{
string file = FileDialogHelper.SaveExcel("成衣工艺单.xls");if (!string.IsNullOrEmpty(file))
{
try{bool success = this.fpSpread1.SaveExcel(file);if(success)
{
if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(file);
}
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}

导出的效果和界面显示的效果基本上一致的,而且图片等特殊的格式,也是正常保留在Excel里面,总体感觉不错。

最后附上这个系统的一些截图作为补充了解。

我在之前一篇文章《
Winform开发框架之框架演化
》中,介绍了传统Winform开发框架、传统WCF开发框架、离线式WCF开发框架、混合式WCF开发框架,其中前面两种就是大家比较熟悉的框架了,后面的离线式WCF开发框架,我在《
Winform开发之离线式WCF开发框架的实现介绍
》一文中也做了阐述,离线式的WCF开发框架,可以看做为传统Winform开发框架+WCF同步模块而成,本文继续探讨这方面的框架设计和实现,重点介绍混合式WCF开发框架的设计思路及具体实现。

Winform开发框架之混合型框架,可以看成是传统winform开发框架和WCF开发框架之间能自由切换的一种双重框架,这种框架的特点是,
就是把系统划分为很多万能模块(既适应WInform集成,也适应WCF集成),在不同的场合进行不同的切换,而且只需通过配置参数的变化就可以实现的跳转,这样非常有利于模块的集成封装。

1、混合型框架的特点

混合型框架具有下面几个特点:

1)环境适应性强,模块可重用性高。由于混合型框架,既可以用于传统Winform系统开发,也可以用于WCF分布式系统开发,因此环境适应性强;而且由于模块具有这些特点,可重用性更高,特别对于通用性的模块,更是具有无可替代的优越性。

2)响应性能更好。如果是Winform程序,那么就使用直接访问数据库方式,如果是WCF调用方式,就使用WCF的专有通道进行数据处理,更好利用系统资源,高效进行数据处理。

3)独立配置,更少的代码修改。所有通用模块,全部通过独立配置文件进行配置WCF的连接,减少主配置文件的复杂性;WCF服务逻辑独立类库,可采用多种服务寄宿方式。

2、混合型框架总体设计思路

Winform开发框架之混合型框架,还是秉承模块化的思路,可以把这个框架分为两大块,一块是主要业务系统模块(如备件管理系统),一块是各种辅助性模块(如通用权限、通用字典、通用附件管理、通用人员管理。。。。),这种两块组合,就是一个完美的系统了。

从以上图可以看到,整个系统的业务系统模块和辅助性模块,都是基于一个思路,通过接口调用开关,决定调用的是WCF服务层,还是Winform业务层(直接访问数据库),当然界面层的调用不管是调用WCF服务层还是Winform业务层,都是基于相同的接口,我们可以把它称为Facade层。辅助性模块则是多种常用模块的组合,他们可能是下面几种的常见模块:通用权限模块、通用字典模块、通用附件管理模块、通用人员管理模块等等。

3、混合型框架具体实现

为了更具体化演绎混合型的Winform开发框架,下面我通过辅助性模块之一的通用字典模块进行介绍这个混合型框架的具体实现。字典模块的内部结构如下所示。

上图的解读如下:

1)共用类库和实体类贯穿整个框架。

2)数据库通过泛型继承方式,实现更少的代码,更丰富的API实现。

3)多数据库支持,通过利用EnterpriseLibrary企业类库,支持多种数据库的集成处理。

4)内置Winform和WCF两种调用实现,通过配置文件,方便自由切换。

5)UI层通过接口调用层的工厂类,实现基于Facade的接口调用(而非具体实现类)。

6)共用UI层,UI层的界面在Winform和WCF调用方式下,均为一致,只有一个UI层。

7)各层均有相应的基类,更少的代码,更多的支持。

8)每个独立模块,构造整个框架的生态体系。

4、项目文件的组成

整个混合型框架的字典模块,按照上面的架构设计,会有不少项目工程产生,由于人的目标识别管理数目有限,因此就单个模块而言,不宜产生过多的项目DLL,否则集成会比较困难,也不适宜更好的维护。因此,基于最少DLL的原则,我设计了下面的模块目录,基本上,每个目录代表一个分层。

由于是上述框架也集成了基于WCF方式的调用方式,那么还需要创建一个WCF的字典服务,我们为了使得WCF支持更多种的寄宿方式,可以建立WCF服务库项目,如下所示这种项目。

创建了相应的分层和逻辑类后,具体的项目工程如下(部分文件由于多个项目中使用到,于是通过引用方式避免拷贝,又能集中管理,如Facade层的接口文件)。

当然还会有一个WCF服务的寄宿方式,这里通过IIS方式发布,如果必要也可以通过其他方式部署WCF服务,由于把逻辑隔离了,因此部署非常方便。

IIS部署方式的WCF服务工程如下所示。

对于IIS方式的部署,其实基本上也是在svc文件中两行代码即可(注意这个SVC文件没有后台.cs文件)

当然,还有一种方式,只需要配置Web.Config,不需要增加svc文件,也能实现WCF服务的部署的哦。

    <serviceHostingEnvironmentmultipleSiteBindingsEnabled="true">
      <serviceActivations>
        <addservice="WHC.Dictionary.WCFLibrary.DictTypeService"relativeAddress="test.svc"/>
      </serviceActivations>
    </serviceHostingEnvironment>

以上就是我对于混合型开发框架的演绎过程,整个框架目前已近全部完成,包括完成了通用权限管理系统模块,通用字典模块,通用附件管理模块,通用人员管理模块等这些外围通用的模块,因此框架的设计是进过实践验证过的,这样的混合型框架,非常适合用于重用性非常高的项目场景中,相比其他类型的框架,更具有高附加值,高可用性的特点。

希望通过我的混合型的框架设计思路和实现逻辑等方面的介绍,抛砖引玉,能和大家做更深的沟通和分析。

DotNetBar是一个不错的DotNET控件套装,原来是一个DLL文件,能够做出很漂亮的界面效果,记得在8.0以前的版本,好像实现多文档界面稍显得麻烦一些,我的Winform框架、WCF框架虽然也提供了这样多文档的界面,不过都是曲线救国的方式实现。随着DotNetBar控件的逐步完善,版本一路飙升,文件也开始学DevExpress那样,使用多个文件进行拆分了。目前11.0版本以上,都有一个SuperTabControl的控件,实现多文档的界面已经很方便了。本文介绍利用SuperTabControl控件实现一个多文档界面的效果,供大家参考学习。

1、多文档界面的设计

下面是框架的一个基于DotNetBar控件的界面设计效果,按照Ribbon样式的方式进行组织,并把多文档界面放在中间,这样界面效果更加美观合理。

另外为了使得在Tab页面上可以关闭窗口,可以增加一个右键菜单,如下所示。

设置控件的相关属性,使得他的关闭按钮一直存在,并关联它的右键菜单即可,如下所示。

这个SuperTabControl,支持好几种Tab样式的,有些看起来非常不错,在其中选择自己喜欢的样式即可。

2、多文档界面的代码实现

在主界面中的Form_Load事件中,我们清空并初始化默认的Tab页面即可,如下所示。

        private void MainForm_Load(objectsender, EventArgs e)
{
Init();
//清空默认的Tab NavTabControl.Tabs.Clear();
tool_ItemDetail_Click(
null, null);
}
        private void tool_ItemDetail_Click(objectsender, EventArgs e)
{
SetMdiForm(
"备件信息", typeof(FrmItemDetail));
}

从上面的代码,我们看到核心的界面排版就是SetMdiForm函数了,下面我们来看看这个函数的具体实现。这个函数目的就是创建或者显示一个多文档界面页面。

        /// <summary>
        ///创建或者显示一个多文档界面页面/// </summary>
        /// <param name="caption">窗体标题</param>
        /// <param name="formType">窗体类型</param>
        public void SetMdiForm(stringcaption, Type formType)
{
bool IsOpened = false;//遍历现有的Tab页面,如果存在,那么设置为选中即可 foreach (SuperTabItem tabitem inNavTabControl.Tabs)
{
if (tabitem.Name ==caption)
{
NavTabControl.SelectedTab
=tabitem;
IsOpened
= true;break;
}
}
//如果在现有Tab页面中没有找到,那么就要初始化了Tab页面了 if (!IsOpened)
{
//为了方便管理,调用LoadMdiForm函数来创建一个新的窗体,并作为MDI的子窗体//然后分配给SuperTab控件,创建一个SuperTabItem并显示 DevComponents.DotNetBar.Office2007Form form =ChildWinManagement.LoadMdiForm(Portal.gc.MainDialog, formType)asDevComponents.DotNetBar.Office2007Form;

SuperTabItem tabItem
=NavTabControl.CreateTab(caption);
tabItem.Name
=caption;
tabItem.Text
=caption;

form.FormBorderStyle
=FormBorderStyle.None;
form.TopLevel
= false;
form.Visible
= true;
form.Dock
=DockStyle.Fill;//tabItem.Icon = form.Icon; tabItem.AttachedControl.Controls.Add(form);

NavTabControl.SelectedTab
=tabItem;
}
}

上面提到了右键菜单的操作,关闭其他或者关闭全部Tab页面的功能,这个实现如下所示。

        private void ctx_Window_CloseAll_Click(objectsender, EventArgs e)
{
CloseAllDocuments();
}
private void ctx_Window_CloseOther_Click(objectsender, EventArgs e)
{
CloseOthers();
}
       public voidCloseAllDocuments()
{
for (int i = NavTabControl.Tabs.Count - 1; i >= 0; i--)
{
SuperTabItem tabitem
= NavTabControl.Tabs[i] asSuperTabItem;if (tabitem != null)
{
tabitem.Close();
}
}
}
public voidCloseOthers()
{
if (ActiveMdiChild != null)
{
Type formType
=ActiveMdiChild.GetType();for (int i = NavTabControl.Tabs.Count - 1; i >= 0; i--)
{
SuperTabItem tabitem
= NavTabControl.Tabs[i] asSuperTabItem;if (tabitem != null && formType != tabitem.AttachedControl.Controls[0].GetType())
{
tabitem.Close();
}
}
}
}

最终界面效果如下所示。

另一个权限管理系统界面的多文档界面调整如下所示。

这样引入了Supertab控件,整体的多文档Tab界面实现起来就更加方便和美观了。

平时工作,多数是开发Web项目,由于一般是开发内部使用的业务系统,所以对于安全性一般不是看的很重,基本上由于是内网系统,一般也很少会受到攻击,但有时候一些系统平台,需要外网也要使用,这种情况下,各方面的安全性就要求比较高了,所以往往会交付给一些专门做安全测试的第三方机构进行测试,然后根据反馈的漏洞进行修复,如果你平常对于一些安全漏洞不够了解,那么反馈的结果往往是很残酷的,迫使你必须在很多细节上进行修复完善。本文主要根据本人项目的一些第三方安全测试结果,以及本人针对这些漏洞问题的修复方案,介绍在这方面的一些经验,希望对大家有帮助。

基本上,参加的安全测试(渗透测试)的网站,可能或多或少存在下面几个漏洞:SQL注入漏洞、跨站脚本攻击漏洞、登陆后台管理页面、IIS短文件/文件夹漏洞、系统敏感信息泄露。

1、测试的步骤及内容

这些安全性测试,据了解一般是先收集数据,然后进行相关的渗透测试工作,获取到网站或者系统的一些敏感数据,从而可能达到控制或者破坏系统的目的。

第一步是信息收集,收集如IP地址、DNS记录、软件版本信息、IP段等信息。可以采用方法有:
1)基本网络信息获取;
2)Ping目标网络得到IP地址和TTL等信息;
3)Tcptraceroute和Traceroute 的结果;
4)Whois结果;
5)Netcraft获取目标可能存在的域名、Web及服务器信息;
6)Curl获取目标Web基本信息;
7)Nmap对网站进行端口扫描并判断操作系统类型;
8)Google、Yahoo、Baidu等搜索引擎获取目标信息;
9)FWtester 、Hping3 等工具进行防火墙规则探测;
10)其他。

第二步是进行渗透测试,根据前面获取到的数据,进一步获取网站敏感数据。此阶段如果成功的话,可能获得普通权限。采用方法会有有下面几种

1)常规漏洞扫描和采用商用软件进行检查;
2)结合使用ISS与Nessus等商用或免费的扫描工具进行漏洞扫描;
3)采用SolarWinds对网络设备等进行搜索发现;
4)采用Nikto、Webinspect等软件对Web常见漏洞进行扫描;
5)采用如AppDetectiv之类的商用软件对数据库进行扫描分析;
6)对Web和数据库应用进行分析;
7)采用WebProxy、SPIKEProxy、Webscarab、ParosProxy、Absinthe等工具进行分析;
8)用Ethereal抓包协助分析;
9)用Webscan、Fuzzer进行SQL注入和XSS漏洞初步分析;
10)手工检测SQL注入和XSS漏洞;
11)采用类似OScanner的工具对数据库进行分析;
12)基于通用设备、数据库、操作系统和应用的攻击;采用各种公开及私有的缓冲区溢出程序代码,也采用诸如MetasploitFramework 之类的利用程序集合。
13)基于应用的攻击。基于Web、数据库或特定的B/S或C/S结构的网络应用程序存在的弱点进行攻击。
14)口令猜解技术。进行口令猜解可以采用 X-Scan、Brutus、Hydra、溯雪等工具。

第三步就是尝试由普通权限提升为管理员权限,获得对系统的完全控制权。在时间许可的情况下,必要时从第一阶段重新进行。采用方法

1)口令嗅探与键盘记录。嗅探、键盘记录、木马等软件,功能简单,但要求不被防病毒软件发觉,因此通常需要自行开发或修改。
2)口令破解。有许多著名的口令破解软件,如 L0phtCrack、John the Ripper、Cain 等

以上一些是他们测试的步骤,不过我们不一定要关注这些过程性的东西,我们可能对他们反馈的结果更关注,因为可能会爆发很多安全漏洞等着我们去修复的。

2、SQL注入漏洞的出现和修复

1)SQL注入定义:

SQL注入攻击是黑客对数据库进行攻击的常用手段之一。随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多。但是由于程序员的水平及经验也参差不齐,相当大一部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断,使应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的SQL Injection,即SQL注入。

SQL注入有时候,在地址参数输入,或者控件输入都有可能进行。如在链接后加入’号,页面报错,并暴露出网站的物理路径在很多时候,很常见,当然如果关闭了Web.Config的CustomErrors的时候,可能就不会看到。

另外,Sql注入是很常见的一个攻击,因此,如果对页面参数的转换或者没有经过处理,直接把数据丢给Sql语句去执行,那么可能就会暴露敏感的信息给对方了。如下面两个页面可能就会被添加注入攻击。

①HTTP://xxx.xxx.xxx/abc.asp?p=YY and (select top 1 name from TestD ... type='U' and status>0)>0 得到第一个用户建立表的名称,并与整数进行比较,显然abc.asp工作异常,但在异常中却可以发现表的名称。假设发现的表名是xyz,则
②HTTP://xxx.xxx.xxx/abc.asp?p=YY and (select top 1 name from TestDB.dbo.sysobjects& ... tatus>0 and name not in('xyz'))>0 可以得到第二个用户建立的表的名称,同理就可得到所有用建立的表的名称。

为了屏蔽危险Sql语句的执行,可能需要对进行严格的转换,例如如果是整形的,就严格把它转换为整数,然后在操作,这样可以避免一些潜在的危险,另外对构造的sql语句必须进行Sql注入语句的过滤,如我的框架(Winform开发框架、Web开发框架等)里面就内置了对这些有害的语句和符号进行清除工作,由于是在基类进行了过滤,因此基本上子类都不用关心也可以避免了这些常规的攻击了。

        /// <summary>
        ///验证是否存在注入代码(条件语句)/// </summary>
        /// <param name="inputData"></param>
        public bool HasInjectionData(stringinputData)
{
if (string.IsNullOrEmpty(inputData))return false;//里面定义恶意字符集合//验证inputData是否包含恶意集合 if(Regex.IsMatch(inputData.ToLower(), GetRegexString()))
{
return true;
}
else{return false;
}
}
/// <summary> ///获取正则表达式/// </summary> /// <returns></returns> private static stringGetRegexString()
{
//构造SQL的注入关键字符 string[] strBadChar ={//"select\\s",//"from\\s", "insert\\s","delete\\s","update\\s","drop\\s","truncate\\s","exec\\s","count\\(","declare\\s","asc\\(","mid\\(","char\\(","net user","xp_cmdshell","/add\\s","exec master.dbo.xp_cmdshell","net localgroup administrators"};//构造正则表达式 string str_Regex = ".*(";for (int i = 0; i < strBadChar.Length - 1; i++)
{
str_Regex
+= strBadChar[i] + "|";
}
str_Regex
+= strBadChar[strBadChar.Length - 1] + ").*";returnstr_Regex;
}

上面的语句用于判别常规的Sql攻击字符,我在数据库操作的基类里面,只需要判别即可,如下面的一个根据条件语句查找数据库记录的函数。

        /// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="condition">查询的条件</param>
        /// <param name="orderBy">自定义排序语句,如Order By Name Desc;如不指定,则使用默认排序</param>
        /// <param name="paramList">参数列表</param>
        /// <returns>指定对象的集合</returns>
        public virtual List<T> Find(string condition, stringorderBy, IDbDataParameter[] paramList)
{
if(HasInjectionData(condition))
{
LogTextHelper.Error(
string.Format("检测出SQL注入的恶意数据, {0}", condition));throw new Exception("检测出SQL注入的恶意数据");
}

...........................
}

以上只是防止Sql攻击的一个方面,还有就是坚持使用参数化的方式进行赋值,这样很大程度上减少可能受到SQL注入攻击。

            Database db =CreateDatabase();
DbCommand command
=db.GetSqlStringCommand(sql);
command.Parameters.AddRange(param);

3、跨站脚本攻击漏洞出现和修复

跨站脚本攻击,又称XSS代码攻击,也是一种常见的脚本注入攻击。例如在下面的界面上,很多输入框是可以随意输入内容的,特别是一些文本编辑框里面,可以输入例如<script>
alert('这是一个页面弹出警告');
</script>这样的内容,如果在一些首页出现很多这样内容,而又不经过处理,那么页面就不断的弹框,更有甚者,在里面执行一个无限循环的脚本函数,直到页面耗尽资源为止,类似这样的攻击都是很常见的,所以我们如果是在外网或者很有危险的网络上发布程序,一般都需要对这些问题进行修复。

XSS代码攻击还可能会窃取或操纵客户会话和 Cookie,它们可能用于模仿合法用户,从而使黑客能够以该用户身份查看或变更用户记录以及执行事务。
[建议措施]
清理用户输入,并过滤出 JavaScript 代码。我们建议您过滤下列字符:
[1] <>(尖括号)
[2] "(引号)
[3] '(单引号)
[4] %(百分比符号)
[5] ;(分号)
[6] ()(括号)
[7] &(& 符号)
[8] +(加号)

为了避免上述的XSS代码攻击,解决办法是可以使用HttpUitility的HtmlEncode或者最好使用微软发布的AntiXSSLibrary进行处理,这个更安全。

微软反跨站脚本库(AntiXSSLibrary)是一种编码库,旨在帮助保护开发人员保护他们的基于Web的应用不被XSS攻击。

编码方法

使用场景

示例

HtmlEncode(String)

不受信任的HTML代码。 <a href=”http://www.cnblogs.com”>Click Here [不受信任的输入]</a>
HtmlAttributeEncode(String)

不受信任的HTML属性

<hr noshade size=[不受信任的输入]>

JavaScriptEncode(String)

不受信任的输入在JavaScript中使用

<script type=”text/javascript”>

[Untrusted input]

</script>

UrlEncode(String)

不受信任的URL

<a href=”http://cnblogs.com/results.aspx?q=[Untrusted input]”>Cnblogs.com</a>

VisualBasicScriptEncode(String)

不受信任的输入在VBScript中使用

<script type=”text/vbscript” language=”vbscript”>

[Untrusted input]

</script>

XmlEncode(String)

不受信任的输入用于XML输出

<xml_tag>[Untrusted input]</xml_tag>

XmlAttributeEncode(String)

不 受信任的输入用作XML属性

<xml_tag attribute=[Untrusted input]>Some Text</xml_tag>

        protected void Page_Load(objectsender, EventArgs e)
{
this.lblName.Text = Encoder.HtmlEncode("<script>alert('OK');</SCRIPT>");
}

例如上面的内容,赋值给一个Lable控件,不会出现弹框的操作。

但是,我们虽然显示的时候设置了转义,输入如果要限制它们怎么办呢,也是使用AntiXSSLibrary里面的HtmlSanitizationLibrary类库Sanitizer.GetSafeHtmlFragment即可。

        protected void btnPost_Click(objectsender, EventArgs e)
{
this.lblName.Text =Sanitizer.GetSafeHtmlFragment(txtName.Text);
}

这样对于特殊脚本的内容,会自动剔除过滤,而不会记录了,从而达到我们想要的目的。

4、IIS短文件/文件夹漏洞出现和修复

通过猜解,可能会得出一些重要的网页文件地址,如可能在/Pages/Security/下存在UserList.aspx和MenuList.aspx文件。

[建议措施]
1)禁止url中使用“~”或它的Unicode编码。
2)关闭windows的8.3格式功能。

修复可以参考下面的做法,或者找相关运维部门进行处理即可。

http://sebug.net/vuldb/ssvid-60252

http://webscan.360.cn/vul/view/vulid/1020

http://www.bitscn.com/network/security/200607/36285.html

5、系统敏感信息泄露出现和修复

如果页面继承一般的page,而没有进行Session判断,那么可能会被攻击者获取到页面地址,进而获取到例如用户名等重要数据的。

一般避免这种方式是对于一些需要登录才能访问到的页面,一定要进行Session判断,可能很容易给漏掉了。如我在Web框架里面,就是继承一个BasePage,BasePage 统一对页面进行一个登录判断。

    public partial classUserList : BasePage
{
protected void Page_Load(objectsender, EventArgs e)
{
...............
    /// <summary>
    ///BasePage 集成自权限基础抽象类FPage,其他页面则集成自BasePage/// </summary>
    public classBasePage : FPage
{
/// <summary> ///默认构造函数/// </summary> publicBasePage()
{
this.IsFunctionControl = true;//默认页面启动权限认证 }/// <summary> ///检查用户是否登录/// </summary> private voidCheckLogin()
{
if (string.IsNullOrEmpty(Permission.Identity))
{
string url = string.Format("{0}/Pages/CommonPage/Login.aspx?userRequest={1}",
Request.ApplicationPath.TrimEnd(
'/'), HttpUtility.UrlEncode(Request.Url.ToString()));
Response.Redirect(url);
}
}
/// <summary> ///覆盖HasFunction方法以使权限类判断是否具有某功能点的权限/// </summary> /// <param name="functionId"></param> /// <returns></returns> protected override bool HasFunction(stringfunctionId)
{
CheckLogin();
bool breturn = false;try{
breturn
=Permission.HasFunction(functionId);
}
catch(Exception)
{
Helper.Alerts(
this, "BasePage调用权限系统的HasFunction函数出错");
}
returnbreturn;
}
protected override voidOnInit(EventArgs e)
{
Response.Cache.SetNoStore();
//清除缓存 base.OnInit(e);

CheckLogin();
}

否则可能会受到攻击,并通过抓包软件发现页面数据,获得一些重要的用户名或者相关信息。

还有一个值得注意的地方,就是一般这种不是很安全的网络,最好要求输入比较复杂一点的密码(强制要求),例如不能全部是数字密码或者不能是纯字符,对位数也要求多一点,因为很多人输入12345678,123456,123这样的密码,很容易被猜出来并登录系统,造成不必要的损失。

6、总结性建议

针对上面发现的问题,提出下面几条建议。

1)在服务器与网络的接口处配置防火墙,用于阻断外界用户对服务器的扫描和探测。
2)限制网站后台访问权限,如:禁止公网IP访问后台;禁止服务员使用弱口令。
3)对用户输入的数据进行全面安全检查或过滤,尤其注意检查是否包含SQL 或XSS特殊字符。这些检查或过滤必须在服务器端完成。
4)关闭windows的8.3格式功能。
5)限制敏感页面或目录的访问权限。