2023年2月

很多时候,我们开发程序都需要使用到对象的XML序列化和反序列化,对象的XML序列化和反序列化,既可以使用XML对象(XmlDocument )进行操作,也可以使用XmlSerializer进行操作,两个各有优点,就操作性而已,我倾向于使用后者来操作XML。本文介绍三种方式操作XML,普通的XDocument的API操作,方便的XmlSerializer对象序列化及反序列化操作,加密XML对象序列化操作。

1、普通的XML对象操作,利用XML对象(XmlDocument )。

一个典型的利用XmlDocument对象操作XML的例子代码如下。

    public static voidRunSnippet()
{
XmlDocument xmldoc
= newXmlDocument ( ) ;
    
//加入XML的声明段落     XmlNode xmlnode = xmldoc.CreateNode ( XmlNodeType.XmlDeclaration , "" , "") ;
    xmldoc.AppendChild (xmlnode ) ;
    
//加入一个根元素     XmlElement xmlelem = xmldoc.CreateElement ( "" , "ROOT" , "") ;
    XmlText xmltext
= xmldoc.CreateTextNode ( "Root Text") ;
    xmlelem.AppendChild ( xmltext ) ;
    xmldoc.AppendChild ( xmlelem ) ;
    
//加入另外一个元素     XmlElement xmlelem2 = xmldoc.CreateElement ("SampleElement") ;
    xmlelem2
= xmldoc.CreateElement ( "" , "SampleElement" , "") ;
    xmltext
= xmldoc.CreateTextNode ( "The text of the sample element") ;
    xmlelem2.AppendChild ( xmltext ) ;
    xmldoc.ChildNodes.Item(
1).AppendChild ( xmlelem2 ) ;
    
//保存创建好的XML文档     try    {
      xmldoc.Save (
"c:\\data.xml") ;
    }
    
catch( Exception e )
    {
      
//显示错误信息       Console.WriteLine ( e.Message ) ;
    }
    Console.ReadLine ( ) ;
}

得到的输出结果如下所示。

<?xml version="1.0"?>
<ROOT>Root Text<SampleElement>The text of the sample element</SampleElement>
</ROOT>

2、使用XmlSerializer进行XML操作

先提供两个对象的序列化和反序列化的封装函数,如下所示。

        /// <summary>
        ///对象序列化XML到文件中/// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="obj">对象</param>
        /// <param name="type">对象类型</param>
        private bool XmlSerialize(string path, objectobj, Type type)
{
XmlSerializerNamespaces ns
= newXmlSerializerNamespaces();
ns.Add(
"", "");try{//如果文件目录不存在,那么创建目录 if (!File.Exists(path))
{
FileInfo fi
= newFileInfo(path);if (!fi.Directory.Exists)
{
Directory.CreateDirectory(fi.Directory.FullName);
}
}
using (Stream stream = newFileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
XmlSerializer format
= newXmlSerializer(type);

format.Serialize(stream, obj, ns);
stream.Close();
}
return true;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
/// <summary> ///XML反序列化/// </summary> /// <param name="path">文件路径</param> /// <param name="type">对象类型</param> /// <returns></returns> private object XmlDeserialize(stringpath, Type type)
{
try{using (Stream stream = newFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
XmlSerializer formatter
= newXmlSerializer(type);
stream.Seek(
0, SeekOrigin.Begin);object obj =formatter.Deserialize(stream);
stream.Close();
returnobj;
}
}
catch{return null;
}
}

注意其中代码

XmlSerializerNamespaces ns = new XmlSerializerNamespaces();

ns.Add("", "");

是把默认的xmlns命名空间多余的内容去掉,这样得到比较干净的XML。

在使用的时候,我们需要定义好对象的实体,这样才能给予对象进行操作,定义的实体类如下所示。

[Serializable]public classUserInfo
{
public string ID { get; set; }public string UserNo{ get; set; }public string UserName { get; set; }public string Sex { get; set; }public string Birthday { get; set; }
}

[Serializable]
public classDataOfUser
{
[XmlElement(ElementName
= "m_User")]public List<UserInfo> list = new List<UserInfo>();
}

调用序列化函数的代码例子如下所示

       private void btnUserNormal_Click(objectsender, EventArgs e)
{
DataOfUser obj
= newDataOfUser();
UserInfo user
= newUserInfo();
user.ID
=Guid.NewGuid().ToString();
user.Sex
= "";
user.UserName
= "张三";
user.UserNo
= "20010001";
user.Birthday
= "1999-1-1";
obj.list.Add(user);

user
= newUserInfo();
user.ID
=Guid.NewGuid().ToString();
user.Sex
= "";
user.UserName
= "李氏";
user.UserNo
= "20020001";
user.Birthday
= "1998-1-1";
obj.list.Add(user);
try{
XmlSerialize(
"C:\\User.xml", obj, obj.GetType());
MessageUtil.ShowTips(
"Ok");
}
catch(Exception ex)
{
MessageUtil.ShowError(ex.Message);
}
}

这样得到的XML内容如下所示。

<?xml version="1.0"?>
<DataOfUser>
  <m_User>
    <ID>f8a8b323-5c56-4c21-9ddf-1cd30f78dfca</ID>
    <UserNo>20010001</UserNo>
    <UserName>张三</UserName>
    <Sex></Sex>
    <Birthday>1999-1-1</Birthday>
  </m_User>
  <m_User>
    <ID>bbb36378-ec27-4e20-ad4b-2d2dc7e142e4</ID>
    <UserNo>20020001</UserNo>
    <UserName>李氏</UserName>
    <Sex></Sex>
    <Birthday>1998-1-1</Birthday>
  </m_User>
</DataOfUser>

反序列化的操作也比较简单,不再赘述。

3)把对象实例化到XML中并进行加密处理

首先我们定义两个序列化加密、解码并反序列化的函数如下所示。

        /// <summary>
        ///XML序列化并加密/// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="obj">对象</param>
        /// <param name="type">对象类型</param>
        /// <returns></returns>
        private bool XmlSerializeEncrypt(string path, objectobj, Type type)
{
XmlSerializerNamespaces ns
= newXmlSerializerNamespaces();
ns.Add(
"", "");try{if (!File.Exists(path))
{
FileInfo fi
= newFileInfo(path);if (!fi.Directory.Exists)
{
Directory.CreateDirectory(fi.Directory.FullName);
}
}
using (Stream stream = newFileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
string content = "";using(MemoryStream ms = newMemoryStream())
{
XmlSerializer format
= newXmlSerializer(type);
format.Serialize(ms, obj, ns);
ms.Seek(
0, 0);
content
=Encoding.UTF8.GetString(ms.ToArray());
}
string encrypt =EncodeHelper.EncryptString(content);byte[] bytes =Encoding.UTF8.GetBytes(encrypt);
stream.Write(bytes,
0, bytes.Length);
stream.Close();
}
return true;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
/// <summary> ///解密并进行XML反序列化/// </summary> /// <param name="path">文件路径</param> /// <param name="type">对象类型</param> /// <returns></returns> private object XmlDeserializeDecrypt(stringpath, Type type)
{
try{string encrypt =File.ReadAllText(path, Encoding.UTF8);string content = EncodeHelper.DecryptString(encrypt, true);byte[] bytes =Encoding.UTF8.GetBytes(content);using (MemoryStream stream = newMemoryStream(bytes))
{
XmlSerializer formatter
= newXmlSerializer(type);

stream.Seek(
0, SeekOrigin.Begin);object obj =formatter.Deserialize(stream);
stream.Close();
returnobj;
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}

这样函数定义好后,调用和前面没有加密的差不多,如下所示。

        private void btnUserEncrypt_Click(objectsender, EventArgs e)
{
DataOfUser obj
= newDataOfUser();
UserInfo user
= newUserInfo();
user.ID
=Guid.NewGuid().ToString();
user.Sex
= "";
user.UserName
= "张三";
user.UserNo
= "20010001";
user.Birthday
= "1999-1-1";
obj.list.Add(user);

user
= newUserInfo();
user.ID
=Guid.NewGuid().ToString();
user.Sex
= "";
user.UserName
= "李氏";
user.UserNo
= "20020001";
user.Birthday
= "1998-1-1";
obj.list.Add(user);
try{
XmlSerializeEncrypt(
"C:\\User-Encrypt.xml", obj, obj.GetType());
MessageUtil.ShowTips(
"Ok");
}
catch(Exception ex)
{
MessageUtil.ShowError(ex.Message);
}
}
private void btnUserDecrypt_Click(objectsender, EventArgs e)
{
string file =FileDialogHelper.OpenFile();if (!string.IsNullOrEmpty(file))
{
DataOfUser info
= XmlDeserializeDecrypt(file, typeof(DataOfUser)) asDataOfUser;if (info != null)
{
MessageUtil.ShowTips(
"OK");
}
}
}

FlexCell表格控件有很多亮点的功能,使用也算比较方便,很多时候,可以模拟传统的Excel内容在Winform界面上展现,而且也支持内容格式的预设置等,本文主要介绍利用这个控件来实现一些特殊的统计及关联信息显示的功能。在实际项目中,有一个这样的需求,首先需要显示一些数据的总的汇总数据,每条又可以进一步查看其对应的明细数据,类似于数据仓库的钻取功能,那么我们要如何实现以上的功能呢?

1、报表统计功能介绍

功能介绍如下图所示。

单击链接可以进一步展现相关的人员列表,如下图所示。

2、功能实现思路分析

实现以上功能,如果利用.NET自带的DataGridView,通过在在Cell里面添加链接控件的方式,估计也能实现,不过可能操作起来会比较麻烦一些。

本文主要介绍利用FlexCell 表格控件实现以上操作的思路,共大家参考,并非为该控件做广告。

首先我们需要在为每个条件显示一行记录,对应还要记住它的条件,其中有一个超链接的单元格,供我们单击可以查看明细。为了记住该行的条件,我其实在一个隐藏列里面放置了条件表达式,真实情况下如下所示,只是为了显示美观,不需要显示隐藏的条件内容。

FlexCell提供超链接的单元格类型,设置为超链接类型的单元格,内容会加下划线,而且可以对超链接的事件进行捕捉处理。

得到了响应的处理事件,以及存放了对应的条件描述,那么在事件里,打开一个新窗体,根据条件内容获取对应的列表显示出来即可。

3、功能实现代码

1)绑定相关的汇总数据

 Dictionary<string, CListItem> dict =GetAgeCondition();if (dict.Keys.Count > 0)
{
grid1.Cell(startRow
++, 1).Text = "年龄段情况:";foreach (string key indict.Keys)
{
CListItem item
=dict[key];
grid1.Cell(startRow,
2).Text =key;
grid1.Cell(startRow,
3).Text = string.Format("有{0}人,占比例{1}", item.Text, item.Value);
grid1.Cell(startRow,
4).Text = string.Format("Age:{0}", key);

startRow
++;
}
}

2)超链接处理事件代码

在控件上单击Grid的Hyperlink处理事件,然后实现其内部处理代码,如下所示。

        private void grid1_HyperLinkClick(objectSender, FlexCell.Grid.HyperLinkClickEventArgs e)
{
FlexCell.Cell cell
= grid1.Cell(e.Row, 4);if (cell != null && !string.IsNullOrEmpty(cell.Text))
{
FrmStatisticDetail dlg
= newFrmStatisticDetail();
dlg.KeyCondition
=cell.Text;
dlg.ShowDialog();
}
e.URL
= "";
e.Changed
= true;
}

3)明细窗体处理代码

       private void DealAge(string condition, ref intstartRow)
{
grid1.Cell(
1, 1).Text += string.Format("(条件:年龄{0})", condition);

startRow
++;//设置单元格文字 grid1.Cell(startRow, 1).Text = "年龄";
grid1.Cell(startRow,
2).Text = "编号";
grid1.Cell(startRow,
3).Text = "姓名";
grid1.Cell(startRow,
4).Text = "性别";
grid1.Cell(startRow,
5).Text = "生日";
grid1.Cell(startRow,
6).Text = "职别";
grid1.Cell(startRow,
7).Text = "部别";
grid1.Range(startRow,
1, startRow, 7).BackColor =Color.Yellow;int start = startRow++;string where = "Age" +condition;if(condition.Contains("~"))
{
string[] conArray = condition.Split('~');where = string.Format("Age >={0} and Age<={1}", conArray[0], conArray[1]);
}

DataTable dt
= BLLFactory<Pilot>.Instance.FindByView("PilotAgeView", where, "Age", true);foreach (DataRow row indt.Rows)
{
grid1.Cell(startRow,
1).Text = row["Age"].ToString();
grid1.Cell(startRow,
2).Text = row["PilotNo"].ToString();
grid1.Cell(startRow,
3).Text = row["Name"].ToString();
grid1.Cell(startRow,
4).Text = row["Sex"].ToString();
DateTime birthday;
if (DateTime.TryParse(row["Birthday"].ToString(), outbirthday))
{
grid1.Cell(startRow,
5).Text = birthday.ToString("yyyy-MM-dd");
}
grid1.Cell(startRow,
6).Text = row["OfficialRank"].ToString();
grid1.Cell(startRow,
7).Text = row["DeptCode"].ToString();

startRow
++;
}
FlexCell.Range range
= grid1.Range(start, 1, startRow, 7);
range.set_Borders(FlexCell.EdgeEnum.Outside
|FlexCell.EdgeEnum.Inside, FlexCell.LineStyleEnum.Thin);

startRow
++;
startRow
++;
}

上面的代码,主要就是动态绘制表头,设置格式,然后绘制表格明细的单元格内容即可。

4)实现打印、导出Excel/PDF等功能

控件内置了很多导出功能,实现基本的导出、打印功能,非常方便,代码如下。

       private void menu_Preview_Click(objectsender, EventArgs e)
{
try{
grid1.PrintPreview();
}
catch(Exception err)
{
MessageDxUtil.ShowError(err.Message);
}
}
private void menu_PageSetting_Click(objectsender, EventArgs e)
{
try{
grid1.ShowPageSetupDialog();
}
catch(Exception err)
{
MessageDxUtil.ShowError(err.Message);
}
}
private void menu_ExportExcel_Click(objectsender, EventArgs e)
{
try{#region 保存Excel文件SpecialDirectories sp= newSpecialDirectories();
SaveFileDialog dialog
= newSaveFileDialog();
dialog.Filter
= "Excel(*.xls)|*.xls|All File(*.*)|*.*";
dialog.Title
= "保存Excel";try{
dialog.InitialDirectory
=sp.Desktop;
}
catch{
dialog.InitialDirectory
= "C:\\";
}
dialog.RestoreDirectory
= true;if (dialog.ShowDialog() ==DialogResult.OK)
{
string fileToSave =dialog.FileName;if (string.IsNullOrEmpty(fileToSave))
{
return;
}
bool success = grid1.ExportToExcel(fileToSave, true, true);if(success)
{
Process.Start(fileToSave);
}
}
#endregion}catch(Exception err)
{
MessageDxUtil.ShowError(err.Message);
}
}
/// <summary> ///导出Excel文件/// </summary> /// <param name="fileToSave">文件路径</param> /// <returns></returns> public bool ExportExcel(stringfileToSave)
{
bool success = grid1.ExportToExcel(fileToSave, true, true);returnsuccess;
}
private void menu_ExportPDF_Click(objectsender, EventArgs e)
{
try{
grid1.PageSetup.DocumentName
= "统计报表";bool success = grid1.ExportToPDF("");
}
catch(Exception err)
{
MessageDxUtil.ShowError(err.Message);
}
}

在我的随笔中,Winform开发介绍过很多内容,随着介绍的深入,很多地方进行精细化的介绍当中了。我们一般在Winform的列表界面中展现相关记录,在一个弹出窗口给客户录入数据,OK,这些没问题,但是我们录入数据后,很多情况下忽略了一些用户体验,等客户自己刷新才在列表中显示最新的记录出来,这样有些注重细节的客户就不愿意了。在我的一个项目开发中,有客户就有这样的抱怨,为什么我录入数据,还要手动刷新,为什么不能一保存就能同时更新列表?好,技术上肯定不是问题,思想考虑不够深入而已。下面我来介绍基于我的Winform开发框架的这种数据即时更新的实现过程。

1、Winform开发框架的界面层的继承关系

在开篇之前,首先介绍一下Winform开发框架中的界面层的继承关系,首先我把所有窗体区分为三类,一类是普通的窗体,一类编辑窗体,还有一类是列表显示窗体。窗体的集成可以封装处理很多东西,良好的封装和继承,可以为我们提高效率,减少重复代码,它的作用不再在此讨论和强调。

采用窗体继承,极大程度上统一了界面,并且对常用的界面操作,提供了良好的封装,如基础数据编辑、新增窗体积累封装了对回车、方向键、数据刷新、异常处理、数据检查、数据保存、数据更新等接口,为窗体的数据处理提供了很大的方便性。
而数据查询显示窗体则考虑到多文档展示的需要,一般继承合适的基类,封装一些常用到的界面布局,以便实现相应的界面处理效果。

上面所述的三类窗体,继承的界面基类分别是下面几个。普通的窗体继承自
BaseForm
,编辑窗体继承自
BaseEditForm
,列表显示窗体继承自
BaseDock
。在工程中如下所示。

在UML设计图中,我们看到他的关系如下所示,其中绿色边框的为我们上面的基类,红色边框的是我们实际的窗体对象,从这个图中,我们可以很清晰看到他的关联关系。

BaseEditForm相对来说,比其他两个基类窗体复杂一些,他除了提供几个数据保存必须的按钮外,还可以通过前后进行数据查看,他的初始化界面及应用例子界面如下所示。

2、Winform开发框架之数据即时更新的实现

介绍完上面的继承关系后,我们来看看基于这种继承模式,如何实现数据及时更新的操作,也就是在数据保存窗体,保存数据后,数据记录同步在列表中展现的效果。

1)首先在基类界面BaseEditForm中添加事件处理

    public partial classBaseEditForm : BaseForm
{
public event EventHandler OnDataSaved;//子窗体数据保存的触发

2)然后添加事件的处理操作,如下所示。

        /// <summary>
        ///处理数据保存后的事件触发/// </summary>
        public virtual void ProcessDataSaved(objectsender, EventArgs e)
{
if (OnDataSaved != null)
{
OnDataSaved(sender, e);
}
}

3)在数据保存的时候,触发即时更新数据的事件

        /// <summary>
        ///保存数据(新增和编辑的保存)/// </summary>
        public virtual boolSaveEntity()
{
bool result = false;if(!string.IsNullOrEmpty(ID))
{
//编辑的保存 result =SaveUpdated();
}
else{//新增的保存 result =SaveAddNew();
}
returnresult;
}
/// <summary> ///更新已有的数据/// </summary> /// <returns></returns> public virtual boolSaveUpdated()
{
return true;
}
/// <summary> ///保存新增的数据/// </summary> /// <returns></returns> public virtual boolSaveAddNew()
{
return true;
}
/// <summary> ///保存/// </summary> /// <param name="close">关闭窗体</param> private void SaveEntity(boolclose)
{
//检查输入的有效性 if (this.CheckInput())
{
//设置鼠标繁忙状态 this.Cursor =Cursors.WaitCursor;try{if (this.SaveEntity())
{
ProcessDataSaved(
this.btnOK, newEventArgs());

MessageDxUtil.ShowTips(
"保存成功");if(close)
{
this.DialogResult =DialogResult.OK;this.Close();
}
else{this.ClearScreen();
}
}
}
catch(Exception ex)
{
this.ProcessException(ex);
}
finally{//设置鼠标默认状态 this.Cursor =Cursors.Default;
}
}
}

以上属于基类的数据保存操作,我们特别注意这段代码即可

 if (this.SaveEntity())
{
ProcessDataSaved(
this.btnOK, new EventArgs());

4)列表展现界面的操作实现

实现上面的操作后,这些就不需要在BaseEditForm的子类做任何操作了,只需要在具体的列表展现界面类中实现下面的代码即可。

我们知道,列表界面一般都会有具体的数据刷新函数封装(如BindData函数),那么我们在新建数据的时候,实现代码是这样的。

        /// <summary>
        ///新增数据操作/// </summary>
        private void btnAddNew_Click(objectsender, EventArgs e)
{
FrmEditLaw dlg
= newFrmEditLaw();
dlg.OnDataSaved
+= newEventHandler(dlg_OnDataSaved);
dlg.ShowDialog();
}
void dlg_OnDataSaved(objectsender, EventArgs e)
{
BindData();
}

这样我们在新窗体(数据添加、数据编辑)中维护数据的时候,一旦触发保存操作,这些最新的数据记录也在列表中展现出来了。

最后放上一个Winform开发框架的界面,供参考。


我们在做Winform项目开发的时候,经常会发现有一些数据很多,需要通过不同的Tab页面分类来实现数据的录入和现实,例如体检数据,可能包含外科、内科、眼科、耳鼻喉科、口腔科、以及其他的检查等等内容,如果一次性放在一个窗口中现实,不太合理也不好看,如果通过多个Tab分类进行管理,则用户体验好很多。

如果分为多个Tab页面进行数据管理,最好的方法是,每个页面负责自己的数据存储及显示,由于数据可能是关联的,存储需要使用事务。保存的时候,可能每项都会有检查数据是否完备及有效性。那么我们应该实现类似这样的界面操作呢。

如上分析,我们把每个模块独立出来做一个控件,如眼科的作为一个独立的用户控件进行展现,如下所示。

其他模块也一样处理,独立作为一个用户控件,然后再在一个主窗体界面中进行整合即可。

为了对窗体能够实现批量处理,我们需要为每个用户控件定义一个接口,他们之间都需要实现这些约定的接口,接口如下所示。

    interfaceIDataApply
{
/// <summary> ///设置控件的重要属性/// </summary> /// <param name="id">记录ID</param> /// <param name="pilotId">飞行人员ID</param> /// <param name="isNewData">是否为新建记录</param> void SetData(string id, string pilotId, boolisNewData);/// <summary> ///数据显示的函数/// </summary> voidDisplayData();/// <summary> ///用于模块事务性提交数据/// </summary> /// <param name="trans">事务对象</param> /// <returns></returns> boolApply(DbTransaction trans);/// <summary> ///检查输入的有效性/// </summary> /// <returns>有效</returns> boolCheckInput();
}

这样,独立的用户控件需要继承接口,实现接口约定的内容。

public partial class EyeControl : DevExpress.XtraEditors.XtraUserControl, IDataApply

我们来看其中一个TabPage的模块接口实现代码如下所示。

        #region IDataApply 成员
                        
        /// <summary>
        ///检查输入的有效性/// </summary>
        /// <returns>有效</returns>
        public boolCheckInput()
{
bool result = true;//默认是可以通过 if (this.txtCheckDate.Text.Length == 0)
{
MessageDxUtil.ShowTips(
"请输入体检结论日期");this.txtCheckDate.Focus();return false;
}
returnresult;
}
/// <summary> ///设置控件的重要属性/// </summary> /// <param name="id">记录ID</param> /// <param name="pilotId">飞行人员ID</param> /// <param name="isNewData">是否为新建记录</param> public void SetData(string id, string pilotId, boolisNewData)
{
this.ID =id;this.Pilot_ID =pilotId;this.IsNewData =isNewData;
}
/// <summary> ///数据显示的函数/// </summary> public voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) if (!this.IsNewData)
{
#region 显示客户信息LargeCheckConclusionInfo info= BLLFactory<LargeCheckConclusion>.Instance.FindByID(ID);if (info != null)
{
txtCheckDate.DateTime
=info.CheckDate;
txtDiagnosis.Text
=info.Diagnosis;
txtResult.Text
=info.Result;
txtSuggestion.Text
=info.Suggestion;
txtHealthLevel.Text
=info.HealthLevel;
txtHospital.Text
=info.Hospital;
}
#endregion }else{
}
}
/// <summary> ///用于模块事务性提交数据/// </summary> /// <param name="trans">事务对象</param> /// <returns></returns> public boolApply(DbTransaction trans)
{
this.trans =trans;bool result = false;if (!this.IsNewData)
{
//编辑的保存 result =SaveUpdated();
}
else{//新增的保存 result =SaveAddNew();
}
returnresult;
}
#endregion

其中我们注意到,各模块的数据显示及保存,都是自治的,这样除了较好管理数据显示及保存外,也使得主界面整合更加简洁方便。

在主编辑界面中,我们需要逐一对各个控件的检查,以及数据保存及显示等操作进行处理,具体我们可以通过遍历进行处理。

1)检查控件输入的部分如下代码所示。

        /// <summary>
        ///检查输入的有效性/// </summary>
        /// <returns>有效</returns>
        public virtual boolCheckInput()
{
bool result = true;//默认是可以通过 if (this.txtPilot.PilotID.Trim().Length == 0)
{
MessageDxUtil.ShowTips(
"请选择飞行人员");this.txtPilot.Focus();
result
= false;
}
//检查输入是否正确 foreach (XtraTabPage page in this.xtraTabControl1.TabPages)
{
foreach (Control control inpage.Controls)
{
IDataApply apply
= control asIDataApply;if (apply != null)
{
apply.SetData(ID,
this.txtPilot.PilotID, IsNewData);
result
=apply.CheckInput();if (!result)
{
this.xtraTabControl1.SelectedTabPage =page;
control.Focus();
}
}
}
}
returnresult;
}

2)显示每个控件负责的数据,操作代码如下所示。

         /// <summary>
        ///显示数据到控件上/// </summary>
        public virtual voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) foreach (XtraTabPage page in this.xtraTabControl1.TabPages)
{
foreach (Control control inpage.Controls)
{
IDataApply apply
= control asIDataApply;if (apply != null)
{
apply.SetData(ID, Pilot_ID, IsNewData);
apply.DisplayData();
}
}
}
}

3)采用事务性保存数据的实现代码如下所示

        /// <summary>
        ///保存数据(新增和编辑的保存)/// </summary>
        public virtual boolSaveEntity()
{
bool result = false;

DbTransaction trans
= BLLFactory<LargeCheckConclusion>.Instance.CreateTransaction();if (trans != null)
{
try{foreach (XtraTabPage page in this.xtraTabControl1.TabPages)
{
foreach (Control control inpage.Controls)
{
IDataApply apply
= control asIDataApply;if (apply != null)
{
apply.SetData(ID,
this.txtPilot.PilotID, IsNewData);
apply.Apply(trans);
}
}
}
if (trans != null)
{
trans.Commit();
result
= true;
}
}
catch(Exception ex)
{
if (trans != null)
{
trans.Rollback();
}

LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
return false;
}
}
returnresult;
}

实现以上的操作,基本上对控件的数据管理就差不多了,这样比我们把全部鸡蛋放到一个篮子里面处理,会显得更加清晰,更加高效,重要的是减少出错的机会,否则想想看,如果操作的数据字段内容上百个,保存,显示,检查这些操作,是不是很容易漏掉,或者出现错误呢。况且把每部分鸡蛋放到一个小篮子里面,我们管理就是小篮子,这样管理的对象减少了,效率也就提高了。

以上就是对于多个Tab页面的数据处理解决方案,应该对大家有一定的启发意义。

WCF开发框架的证书加密操作,前面在框架之旅做了一篇介绍,本文主要是将各种相关知识点综合起来进行介绍,以期达到系统化介绍在我的WCF开发框架(也可以应用于您的WCF框架)中使用证书加密的常规操作和步骤。在WCF应用日益广泛的环境下,注意数据的安全性以及系统稳定性的问题,是我们设计初期必须考虑的重要举措之一,特别对一些行业比较敏感的数据或者重要机关的数据,这个是一个硬性规定的前提条件之一。在WCF开发框架中,应用X.509证书加密可以解决这个问题,但是要使用这个东西,我们必须对相关的知识点有所了解,然后对相关的配置进行设置,才能正常进行使用。

1、 证书加密应用背景

在网络化的环境中,特别是基于互联网发布的WCF分布式应用程序,数据的加密传输就显得非常重要,通过对网络数据的加密可以有效保障系统的安全可靠性、私密性。

默认情况下,WCF调用是不经过数据加密的。通过默认操作实现的服务以及客户端调用,数据传输情况是如何的。我这里通过HTTP网络包分析工具Http Analyzer来对数据进行监控看看,如下所示。


可以看到,输入及输出的结果全部是明文,数据传输没任何秘密可言。当然,你也可以对这些数据自己加密后发送出去,但是每个接口,大量信息也要这样自己手工加密吗,难道微软的WCF那么弱智?

当然不是,这些操作交给WCF处理会便捷很多,传说中有一个X509的证书加密,不过如果是第一次搞这个,要完全调通要走不少弯路,我就是参考了很多文章(很多文章都介绍了一部分内容,很少能系统性的给你提出完全的解决方法),遇鬼杀鬼,遇佛拜佛,逐步排除各种险阻,这样慢慢才有一个清晰的思路,逐步解决问题,最终才有心思来写这篇文章,后面的随笔我会介绍完整的解决思路来处理X509加密的操作。

WCF常用的就是使用X509证书来实现加密,通过X509加密处理后,我们看到下面的效果。


里面的内容,包括输入及获取到的输出内容,均被加密的乱七八糟,一塌糊涂,即使是在公网中,看到也是白搭,不知是美女还是野兽。实现以上的加密操作,在调用上,完全不会受影响,还是和原来的调用一样,不过需要在WCF的服务以及调用的客户端进行一些文件配置等操作即可。

综上所述,本文介绍的WCF框架的证书加密操作,具有下面几个特点:

1)不需增加额外的代码,通过外置配置即可实现对应的加解密操作。

2)数据传输字段及内容均已进行加密,加密是通过证书对(服务器客户端各一个)进行处理。

3)高配置型、灵活性的实现方式。

2、WCF框架中使用证书加密注意事项

1) 我们一般通过Windows内置工具或者命令行创建的证书,其实是临时用的测试证书,如果在配置文件的Certificate ValidationMode属性中使用除了“None”属性外的值,就不能顺利运行调用服务。这个问题其实可以通过在Windows2003中部署证书服务来创建合格的证书,不过需要先搭建好证书服务平台(或者花钱购买)。

2) 开发机器上部署服务,如果操作系统是XP系统的话,不用考虑给IIS授权的问题。如果是在Windows2003等服务器机器上,要给IIS授权才能访问证书,需要使用WinHttpCertCfg.exe或者CertMgr.exe来实现证书的授权等操作。

3、证书加密的开发步骤

综上所述,要成功部署基于X509证书加密的操作,需要这几方面的知识和操作步骤。

1)   创建WCF服务应用以及调用客户端。

2)   创建客户证书和服务端的X509证书

3)   配置服务端的配置文件,使之使用X509证书服务。

4)   授权IIS访问服务器证书

5)   在服务器证书管理中导入服务器端证书

在客户端导入客户端证书,完成X509证书配置并调用服务。

3.1 创建WCF服务应用以及调用客户端

这个是基本的操作,客户可以创建适合自己的WCF开发框架,这里介绍的WCF开发框架,时基于我的Winform开发框架扩展而成的WCF框架,是一种比较完善、高效、弹性化的开发框架,详细步骤及说明请参考随笔《基于我的Winform开发框架扩展而成的WCF开发框架》,使用这种方式构造的开发框架,各层职责比较分明,而且方便管理。

架构视图如下所示:


项目工程截图如下所示(参考视图):


调用客户端需要添加WCF服务的引用,操作如下所,注意一般约定的命名规则为
ABC
Service
Reference


框架大量使用了基于泛型的数据传输接口,因此创建服务调用的客户端,需要在【高级】窗口设置中,添加泛型的转换接口设置(在集合类型中选择System.Collections.Generic.List,其他默认),如下图所示。

3.2 创建客户证书和服务端的X509证书


VS2008

DOS
命令提示中,输入下面的指令即可创建相关的服务器和客户端证书。

makecert.exe -sr LocalMachine -ss My -a sha1 -n CN=ParkingServer-sky exchange –pe
makecert
.exe -sr LocalMachine -ss My -a sha1 -n CN=ParkingClient-sky exchange -pe

下面是各种参数的介绍

属性

解析

-sr

指定的证书存储区中的注册表位置。
currentUser
指定注册版存储位置为 HKEY_CURRENT_USER.
localMachine
指定注册版存储位置为 HKEY_LOCAL_MACHINE.

-ss

指定证书存储的位置。

-a

指定相关的算法,可以选择 MD5 算法或者 SHA1算法

-n

指定证书的名称。该名称遵循X.500命名标准。简单例子如 "CN=MyName" 格式,如果没有指定/n开关,证书默认的名称是"Joe's Software Emporium"。

-sky

证书键类型。可以设置为 exchange 或者 signature。

-pe

证书可导出


创建证书成功后,我们在开发的机器上就可以看到两个不同的证书了。

要看这两个证书,需要执行下面操作, 开始-》运行-》mmc.exe=》在控制台中选择文件=》添加/删除管理单元=》添加=》选择证书=》弹出证书管理单元选择计算机账户,默认下一步确定回来,就看到下面的界面。

在上面我们可以看到创建在LocalMachine账户下的证书,我们双击查看证书可以看到这个证书是一个不受信任的证书,但是我们还是可以使用的,只是需要在Web.Config配置信息中,certificateValidationMode 属性指定为
None
,而不能指定其他信任值。

3.3 配置服务端的配置文件,使之使用X509证书服务

根据创建的证书信息,我们修改Web.Config文件,使之应用X509证书加密,如下所示。关键的地方是设置验证方式使用证书。

 <securitymode="Message"> 
     <messageclientCredentialType="Certificate"/>
</security>

而证书的相关信息指定如下。

<serviceCertificatefindValue="ParkingServer"x509FindType="FindBySubjectName"storeLocation="LocalMachine"storeName="My"/> 

上面标示证书名称是ParkingServer ,通过查找名字方式对应,并且存储在本地计算机账户、个人证书目录下的位置。

Web.Config 部分内容如下所示。


3.4 授权IIS访问服务器证书

完成以上操作后,如果你的开发机器是XP,那么应该服务端是可以运行正常了,如果你的开发机器是Window2003,那么,恭喜你中奖了,运行服务后会出现下面的错误信息的:[ArgumentException: 证书“CN=ParkingServer”必须具有能够进行密钥交换的私钥。该进程必须具有访问私钥的权限。如下图所示。


要解决上面的错误, 只需要给相应的账号分配权限即可,这里IIS访问证书密钥的权限可以通过
WinHttpCertCfg.exe
来处理,只需要通过命令行执行该程序即可,如下所示。

winhttpcertcfg -g -c LOCAL_MACHINE\My -s ParkingServer -a "NETWORKSERVICE" 

运行命令后,在运行WCF服务,一切OK了


这里另外也介绍下
WinHttpCertCfg.exe
来进行处理的几个命令。

列出证书的授权列表
winhttpcertcfg -l -c LOCAL_MACHINE\My -s ParkingServer -a "NETWORKSERVICE"

给IIS的NETWORKSERVICE账户以基于证书名称模式授权
winhttpcertcfg -g -c LOCAL_MACHINE\My -s ParkingServer -a "NETWORKSERVICE"

给IIS的NETWORKSERVICE账户以基于证书文件模式授权
winhttpcertcfg -g -i "ParkingServer.pfx" -c LOCAL_MACHINE\My -a "NETWORKSERVICE" -p

移除账号对于证书的授权

winhttpcertcfg -r -c LOCAL_MACHINE\My -s ParkingServer -a "NETWORKSERVICE"

3.5 在服务器证书管理中导入服务器端证书

完成以上步骤,如果服务配置文件正确,基本上在开发机器上不会有什么问题了,但是如果把服务部署到另外一台服务器机器上,那么就会可能出现找不到证书的错误。因为服务还没有导入创建好的证书呢。
首先我们在开发机器上,在证书的控制台中选定服务端证书并导出即可把服务端证书导出到文件中,如下图所示。


记得在导出的时候选择带私钥的选项即可,导出的文件保存为ParkingServer.pfx。然后我们在服务器中,在相同的控制台中把服务端的证书导入即可使得WCF服务在服务器中也可以正常访问了。

3.6 在客户端导入客户端证书,完成X509证书配置并调用服务

如果客户端需要部署很多个,那么每个客户端需要把第二步创建的客户端证书导入,才能正常方便服务器的WCF服务,否则会出现证书的各种提示错误,导入操作如下所示。

如果这些操作完成,那么真诚恭喜你,真的可以使用WCF服务了,而且是使用了X509证书加密的WCF服务。

另外,为了使用X.509证书服务,客户端的配置文件确保如下所示,注意证书部分的设置即可。


关于我的Winform开发框架和WCF开发框架的特点,可以访问网站介绍地址(
http://www.iqidi.com/Framework/info.html?flag=3
)进行了解.