2023年2月

好久没有继续跟进淘宝的API使用了,有很多做相关应用的同行都来咨询,很多都因为自己开发工作比较忙而没有来得及好的处理,前几天,有一个朋友叫帮忙指导如何使用淘宝API,由于原来有一些成熟的例子应用,因此原以为直接运行或者稍微修改一下即可,因此没有太多的注意。

可是,发现淘宝的API应用变化已经很大了,离我最后一篇淘宝API例子的随笔《
淘宝API开发系列---淘宝API的测试及使用2

》有一年多的时间了,因此重新到
http://api.taobao.com/myresources/standardSdk.htm
里面下载最新的SDK进行更新调整,可是里面很多接口已经有所变化,有些已经在客户端SDK里面不再支持。

首先我们需要申请一个应用

特别的是,现在的淘宝API客户端应用,也只有公司账号才能使用,普通的淘宝用户只能使用Web应用方式,貌似如果不是选择客户端应用,那么使用Winform程序,将不能正确获取相关的API信息。我使用公司账号调整一个应用为客户端应用。

我在我原来例子的基础上,根据最新的SDK进行了修改,有些如不需要SessionKey获取用户信息的接口已经不能使用了,例如客户的买入的订单记录也不能使用了,而且新增了很多相关的接口内容。

为了更好对API的使用,我们需要使用淘宝API的测试工具进行测试相关的接口,如下界面所示。

对于需要获取用户私密信息,如店铺及交易等重要信息,还需要获取用户的SessionKey的,我们可以通过下面接口函数,弹出登录窗口,然后登录后,定位到对应的App应用页面,然后页面加载的时候,获取到对应的SessionKey。

通过上面两个对话框的登录和确认操作,获取到TOP授权码,并根据规则获得SessionKey了,这样一些重要数据就可以通过获取其中的SessionKey进行获取了。

首先我们来看看几个功能模块的界面效果和操作代码。

1)获取登录用户的信息

主要代码如下所示:

            if (string.IsNullOrEmpty(sessionKey))
{
sessionKey
= GetAuthorizeCode(this.appkey);
}
//用户的信息 Console.WriteLine("用户的信息");
UserSellerGetRequest req
= new UserSellerGetRequest(); //实例化具体API对应的Request类 req.Fields = "user_id,nick,created,buyer_credit,type,sex";
UserSellerGetResponse rsp
= myclient.Execute(req, sessionKey);//执行API请求并将该类转换为response对象 Console.WriteLine(rsp.Body);if (rsp.User != null)
{
//Console.WriteLine(rsp.User.Nick);//Console.WriteLine(ObjectToString(rsp.User)); List<User> list = new List<User>();
list.Add(rsp.User);
this.winGridView1.DisplayColumns = req.Fields.Replace("_", "");//对应字段的属性没有“_”字符的 this.winGridView1.DataSource =list;
}

2)获取单独商品信息

获取单独的产品信息不需要SessionKey,具体代码如下所示。

            //单独商品的信息
            Console.WriteLine("单独商品的信息");
ItemGetRequest req
= newItemGetRequest();
req.Fields
= "num_iid,title,nick,pic_path,cid,price,type,location.city,delist_time,post_fee";
req.NumIid
= 10838073156L;//3838293428L; ItemGetResponse itemRsp =myclient.Execute(req);if (itemRsp != null && itemRsp.Item != null)
{
List
<Item> list = new List<Item>();
list.Add(itemRsp.Item);
this.winGridView1.DisplayColumns = req.Fields.Replace("_", "");//对应字段的属性没有“_”字符的 this.winGridView1.DataSource =list;
}

3)获取店铺商品列表

开发淘宝应用,很多时候,用于本公司的ERP或者产品管理系统与淘宝店铺的数据同步,因此需要获取自己商店的一些商品数据,然后进行修改更新等操作。获取商品信息主要代码如下所示。

            if (string.IsNullOrEmpty(sessionKey))
{
sessionKey
= GetAuthorizeCode(this.appkey);
}
//查询店铺商品信息 Console.WriteLine("查询店铺商品信息");
ItemsOnsaleGetRequest req
= newItemsOnsaleGetRequest();
req.Fields
= "approve_status,num_iid,title,nick,type,cid,pic_url,num,props,valid_thru,list_time,price,has_discount,has_invoice,has_warranty,has_showcase,modified,delist_time,postage_id,seller_cids,outer_id";
req.PageNo
= 1;
req.PageSize
= 40;//显示列表信息 ItemsOnsaleGetResponse itemRsp =myclient.Execute(req, sessionKey);if (itemRsp != null)
{
this.winGridView1.DisplayColumns = req.Fields.Replace("_", "");//对应字段的属性没有“_”字符的 this.winGridView1.DataSource =itemRsp.Items;
}

4)获取店铺卖出交易信息

店铺的卖出交易信息获取的代码如下所示。

            if (string.IsNullOrEmpty(sessionKey))
{
sessionKey
=GetAuthorizeCode(appkey);
}
//卖出交易 Console.WriteLine("卖出交易");
TradesSoldGetRequest req
= newTradesSoldGetRequest();
req.Fields
= "tid,title,price,type,num_iid,seller_nick,buyer_nick,status,receiver_state,receiver_city,receiver_district,receiver_address";
req.PageNo
= 1L;
req.PageSize
= 40L;
TradesSoldGetResponse soldRsp
=myclient.Execute(req, sessionKey);if (soldRsp != null)
{
this.winGridView1.DisplayColumns = req.Fields.Replace("_", "");//对应字段的属性没有“_”字符的 this.winGridView1.DataSource =soldRsp.Trades;
}

以上就是一些基础淘宝API的使用说明,当然,完善的淘宝应用系统客户,可能需要做更多的东西,如商品信息同步、库存同步、自动评价、自动发货等操作,不过基本的操作思路是很接近的,只需要进一步查找淘宝API的使用说明,以及一些经验代码即可。

在这个纷繁的社会里面,统一性的特点能够带来很多高效的产出、牢固的记忆,这种特征无论对于企业、个人的开发工作,知识的传承都有着非常重要的作用,Winfrom框架本身就是基于这个理念而生,从统一的数据库设计规则开始,统一的项目格局,统一的业务类、数据访问类、实体类继承关系,再到统一的公用类库,统一的权限管理模块,统一的字典管理模块,统一的附件管理...,理解这些理念和规则后,再来个终极的统一,框架代码快速生成--Database2Sharp代码生成工具。所有的框架(包括传统Winform开发框架、WCF开发框架、混合式开发框架、Web开发框架)都融合到这里来,统一整合各种看似零散,实则高度渗透的模块,统一的步伐除了时间、效力外,带来给我更多的馈赠。本文主要介绍我的Winform框架(也包括其他框架的特点)的业务对象统一调用的方式,介绍如何通过BLLFactory或者CallerFactory的公用类库来实现各种业务对象的创建工作。

1、BLLFactory的对象统一调用规则

在我的框架里面,所有的业务类调用都是以BLLFactory入口进行开始创建,传递业务对象进去即可创建,这种统一入口的方式能够方便记忆,并减少代码,更重要的是能够很好把一些如缓存规则、创建规则封装起来,简化代码。BLLFactory的创建示意图如下所示。

既然是统一调用规则方式,那么BLLFactory的类库就应该提升到公用类库的级别,所以提供Winform框架支持的公用类库如下所示。

当然,为了减少代码,提高开发效率,整体的框架处处体现了代码重用的规则,尽可能把重复的代码提取出来,因此还有很多如数据访问基类、业务访问基类、数据访问基类接口、实体基类等类库,结合泛型能够使我们的API更加统一化、强类型化,提高开发效率。由于BLLFactory是公用类库级别,所有独立开发的模块,也都是以该类库为统一入口,创建所必须的对象。

我们看看框架如何能够在纷繁复杂的类库里面,准确创建一系列的对象的。

BLLFactory业务对象工厂辅助类的代码如下所示。

    /// <summary>
    ///对业务类进行构造的工厂类/// </summary>
    /// <typeparam name="T">业务对象类型</typeparam>
    public class BLLFactory<T> where T : class{private static Hashtable objCache = newHashtable();private static object syncRoot = newObject();/// <summary>
        ///创建或者从缓存中获取对应业务类的实例/// </summary>
        public staticT Instance
{
get{string CacheKey = typeof(T).FullName;
T bll
= (T)objCache[CacheKey];  //从缓存读取 if (bll == null)
{
lock(syncRoot)
{
if (bll == null)
{
bll
= Reflect<T>.Create(typeof(T).FullName, typeof(T).Assembly.GetName().Name); //反射创建,并缓存 objCache.Add(typeof(T).FullName, bll);
}
}
}
returnbll;
}
}
}

其中利用了哈希表对象对创建的对象进行缓存,并进一步传递参数给Reflect公用类库,对指定对象全名、程序集的业务对象进行创建。

以上只是很薄的一层关系,一般我们都能够很容易理解,但是我们知道,每个业务对象类,还需要负责创建里面的数据访问基类(如IUser接口的数据访问基类userDal),我们来继续分析BaseBLL对象的工作逻辑,才能很好理解其中的关系。

    /// <summary>
    ///用户信息业务管理类/// </summary>
    public class User : BaseBLL<UserInfo>{privateIUser userDal;public User() : base()
{
base.Init(this.GetType().FullName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);this.userDal = (IUser)base.baseDal;
}
..............................

}

其中Init的函数接口定义如下,主要就是根据相关参数,构建数据不同的数据访问对象,如SqlServer的访问对象,或者Oracle的数据访问对象等。

        /// <summary>
        ///参数赋值后,初始化相关对象/// </summary>
        /// <param name="bllFullName">BLL业务类的全名(子类必须实现),子类构造函数传入this.GetType().FullName</param>
        /// <param name="dalAssemblyName">数据访问层程序集的清单文件的文件名,不包括其扩展名,可使用Assembly.GetExecutingAssembly().GetName().Name</param>
        /// <param name="bllPrefix">BLL命名空间的前缀(BLL.)</param>
        void Init(string bllFullName, string dalAssemblyName, string bllPrefix = "BLL.")

由于数据访问也是基于反射方式(带缓存)创建,因此需要知道数据访问类的全名和对应的程序集,如果整合在一个项目工程里面,如我的框架结构代码所示,那么dalAssemblyName就是System.Reflection.Assembly.GetExecutingAssembly().GetName().Name了。其中Init函数主要就是根据配置的数据库类型,创建指定类型的数据访问业务对象,主要的逻辑代码如下所示。

            #region 根据不同的数据库类型,构造相应的DAL层AppConfig config= newAppConfig();string dbType = config.AppConfigGet("ComponentDbType");if (string.IsNullOrEmpty(dbType))
{
dbType
= "sqlserver";
}
dbType
=dbType.ToLower();string DALPrefix = "";if (dbType == "sqlserver")
{
DALPrefix
= "DALSQL.";
}
else if (dbType == "access")
{
DALPrefix
= "DALAccess.";
}
else if (dbType == "oracle")
{
DALPrefix
= "DALOracle.";
}
else if (dbType == "sqlite")
{
DALPrefix
= "DALSQLite.";
}
else if (dbType == "mysql")
{
DALPrefix
= "DALMySql.";
}
#endregion this.dalName = bllFullName.Replace(bllPrefix, DALPrefix);//替换中级的BLL.为DAL.,就是DAL类的全名 baseDal = Reflect<IBaseDAL<T>>.Create(this.dalName, dalAssemblyName);//构造对应的DAL数据访问层的对象类

由于BLLFactory<T>通过传入指定的业务对象,成功创建后,会返回相关联的类的实例,因此接口的调用在设计时就知道了,也就是上面例子里面的BLLFactory<BLL.User>.Instance就是BLL.User类的实例,具有所有该类的接口方法,实现了强类型API的目的了。

下面就是该类在实际界面项目里面的使用例子代码。

                try{
UserInfo info
= BLLFactory<User>.FindByID(currentID) asUserInfo;if (info != null)
{
info
=SetUserInfo(info);
BLLFactory
<User>.Instance.Update(info, info.ID.ToString());
RefreshTreeView();

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

2、CallerFactory的对象统一调用规则

CallerFactory对象是用于创建基于Facade层接口的业务对象,主要用在我的WCF开发框架、混合式开发框架里面,该对象的创建逻辑类似于BLLFactory,不过它创建的对象,可能是基于WCF客户代理的对象,也可能是对BLLFactory创建对象的进一步封装,以便实现宏观上的统一。如下图所示,CallerFactory处在下面框架结构图的中间部分,UI层的下面。

CallerFactory的调用代码例子如下所示。

{
UserInfo info
= CallerFactory<IUserService>.Instance.FindByID(currentID);if (info != null)
{
info
=SetUserInfo(info);
CallerFactory
<IUserService>.Instance.Update(info, info.ID.ToString());
RefreshTreeView();

MessageDxUtil.ShowTips(
"资料保存成功");
}
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}

我们看到,虽然上面的代码是基于WCF的分布式应用,我们还是可以看到,这个调用的思路和方式,和传统Winform的BLLFactory如出一辙,能解决对象调用问题的同时,这样的操作方式,能够给我们学习框架提供了更好的统一模式,顺利切换,而且,从传统Winfrom开发框架的界面代码迁移到分布式应用的WCF开发上,界面代码的变化也是很有规律的,这就是统一模式的力量和奥妙所在。

和BLLFactory里面传入的业务层对象不同,这里CallerFactory里面传入的对象是Facade层的接口,那么它是如何知道我们要创建的对象,并把它转换为我们需要的接口的呢?

    /// <summary>
    ///混合式框架或WCF框架中针对不同调用方式的工厂类(WCF或者Win调用)/// </summary>
    /// <typeparam name="T">接口类型</typeparam>
    public class CallerFactory<T>{private static Hashtable objCache = newHashtable();private static object syncRoot = newObject();private static string callerNamespace = null;//Facade接口实现类的命名空间

        /// <summary>
        ///创建或者从缓存中获取对应接口的实例/// </summary>
        public staticT Instance
{
get{string CacheKey = typeof(T).FullName;
T bll
= (T)objCache[CacheKey];  //从缓存读取 if (bll == null)
{
lock(syncRoot)
{
if (bll == null)
{
bll
= CreateObject(); //反射创建,并缓存 if (bll != null)
{
objCache.Add(
typeof(T).FullName, bll); //缓存BLL业务对象 }
}
}
}
returnbll;
}
}

从上面的代码我们看到,这里的创建逻辑和BLLFactory很大程度的相同,只是细节部分,我使用了CreateObject 进行了隔离,放到独立的函数里面进行创建了。

CreateObject 函数主要逻辑就是根据WCF框架配置信息,到具体的程序集里面创建对应的对象实例,然后转换为Facade层接口,方便统一调用。这就是我WCF开发框架和混合式开发框架,统一调用接口进行通讯获取或提交数据的工作模式。

以上就是我Winform开发框架、WCF开发框架、混合式开发框架、Web开发框架里面所用到的两种方式的对象创建方式的说明,希望您能从统一的调用方式可以看到更多的奥妙及好处。

关于以上几种框架的定义说明,请查看下面图示的介绍,打开图示可以查看更多的框架介绍内容。

1、通用数据导入导出操作模块回顾

在我的Winfrom开发框架里面,有一个通用的导入模块,它在默默处理这把规范的Excel数据导入到不同的对象表里面,一直用它来快速完成数据导入的工作。很早在随笔《
Winform开发框架之通用数据导入导出操作
》里面就很全面的介绍过它的相关功能了,在代码生成工具Database2Sharp里面,生成的Winfrom界面代码也已经把它的调用代码放进去了,因此使用起来真是很好,很开心。

在不断的项目实践中,发现使用基于Sqlite的客户端作为单机版的操作也越来越多,因此大批量的数据导入,也是经常碰到的事情,我们知道,SqlServer批量插入数据会很快,即使你没有使用事务,一条条的插入,大批量也会比较快,这个可能得益于SqlServer本身的事务优化效果。但是作为单机版的数据库,Sqlite每次操作都是单独一个事务的,插入一条数据效率可能不明显,如果操作一千条,一万条,数据的缓慢就很明显,甚至不可忍耐了。我曾经在《
使用事务操作SQLite数据批量插入,提高数据批量写入速度,源码讲解
》里面提到了批量插入通用字典模块的字典数据,使用事务前后批量插入数据,那个速度可是差别很大。

基于以上的因素考虑,决定对通用的数据导入模块进行事务性的优化,以便适应我频繁使用Sqlite数据库大批量导入数据的情况,提高客户的良好体验。本篇主要基于事务性操作的完善,实现基于Sqlite数据的批量快速导入操作。

2、事务性代理事件的定义

由于是通用的模块,所以我们不知道具体的数据库事务对象,但是我们能够通过定义一些事件,给调用者进行事务对象的传递,这样才能在基类中使用事务对象,首先我们定义两个委托事件,一个是SaveDataHandler,用来进行单条数据的处理委托,一个是CreateTransactionHandler,让调用者创建并传递事务对象的委托,具体代码如下所示。

    public partial classFrmImportExcelData : BaseForm
{
...............................
private DbTransaction transaction = null;/// <summary> ///使用事务对数据进行保存的委托,加快速度/// </summary> /// <param name="dr">数据行</param> /// <param name="trans">事务对象</param> /// <returns></returns> public delegate boolSaveDataHandler(DataRow dr, DbTransaction trans);/// <summary> ///创建事务对象的委托,在导入操作初始化的时候赋值/// </summary> /// <returns></returns> public delegate DbTransaction CreateTransactionHandler();

定义好委托后,我们需要创建对应委托的事件对象,作为通用模块的事件,如下所示。

        /// <summary>
        ///保存数据事件/// </summary>
        public eventSaveDataHandler OnDataSave;/// <summary>
        ///刷新数据事件/// </summary>
        public eventEventHandler OnRefreshData;/// <summary>
        ///让调用者创建事务并传递给通用模块/// </summary>
        public event CreateTransactionHandler OnCreateTransaction;

在实现数据导入前,我们需要使用事件来获取对应的事务对象,以便开始事务,具体代码如下所示。

            if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") ==DialogResult.Yes)
{
if (myDs != null && myDs.Tables[0].Rows.Count > 0)
{
DataTable dt
= myDs.Tables[0];this.progressBar1.Visible = true;if (!worker.IsBusy)
{
if (OnCreateTransaction != null)
{
transaction
=OnCreateTransaction();
}
worker.RunWorkerAsync();
}
}
}

3、事务处理逻辑及调用者使用逻辑

这样,我们在通用模块里面,获取到Excel数据后,需要遍历每行数据,然后通过事务对象实现数据提交,部分代码如下所示。

                    #region 批量保存数据,然后事务提交
                    foreach (DataRow dr indt.Rows)
{
if (OnDataSave != null)
{
try{bool success = OnDataSave(dr, transaction);if(success)
{
itemCount
++;
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
int currentStep = Convert.ToInt32(step *i);
worker.ReportProgress(currentStep);
i
++;
}
#endregion if (transaction != null)
{
transaction.Commit();
}

我们看到,在通用的导入模块里面,我们只看到传递事务对象给OnDataSave(dr, transaction)事件,并最终提交整个事务处理而已,具体的

从以上的代码看到,我们把创建事务对象的方法留给调用者实现OnCreateTransaction事件接口,保存每行数据,也留给调用者实现数据的保存OnDataSave事件。

具体的模块调用代码如下所示。

        private string moduleName = "药品目录";private void btnImport_Click(objectsender, EventArgs e)
{
string templateFile = string.Format("{0}-模板.xls", moduleName);
FrmImportExcelData dlg
= newFrmImportExcelData();
dlg.SetTemplate(templateFile, System.IO.Path.Combine(Application.StartupPath, templateFile));
dlg.OnDataSave
+= new FrmImportExcelData.SaveDataHandler(ExcelData_OnDataSave);
dlg.OnCreateTransaction += new
FrmImportExcelData.CreateTransactionHandler(dlg_OnCreateTransaction);
dlg.OnRefreshData
+= newEventHandler(ExcelData_OnRefreshData);
dlg.ShowDialog();
}

DbTransaction dlg_OnCreateTransaction()
{
return BLLFactory<DrugDetail>.Instance.CreateTransaction();
}
void ExcelData_OnRefreshData(objectsender, EventArgs e)
{
BindData();
}
boolExcelData_OnDataSave(DataRow dr, DbTransaction trans)
{
string drugNo = dr["药品编码"].ToString();string drugName = dr["药品名称"].ToString();if (string.IsNullOrEmpty(drugNo) && string.IsNullOrEmpty(drugName))return false;bool success = false;
DrugDetailInfo info
= newDrugDetailInfo();
info.DrugNo
=drugNo;
info.DrugName
=drugName;
info.Manufacture
= dr["制造商"].ToString();
info.Formulations
= dr["剂型"].ToString();
info.Specification
= dr["规格"].ToString();
info.Unit
= dr["药品单位"].ToString();
info.Note
= dr["备注信息"].ToString();
info.StockQuantity
= ConvertHelper.ToInt32(dr["库存量"].ToString(), 0);

info.EditTime
=DateTime.Now;
info.Editor
=Portal.gc.LoginInfo.Name;
info.Dept_ID
=Portal.gc.LoginInfo.Dept_ID;
success
= BLLFactory<DrugDetail>.Instance.Insert(info, trans);returnsuccess;
}

写到这里,可能很多时候大家觉得随笔应该画上句号了吧,其实不然,还有很重要一个地方,需要提及一下,就是我们使用了事务保存数据,那么如果需要在单条记录保存的时候,需要判断检索数据,才决定插入还是更新操作呢?

如果你觉得随便写一个select语句调用不就可以了吗?那样可能就会有问题了,事务性操作会锁定当前的表,不会让你继续写入了,很快就会得到操作超时的错误异常了。

那么我们应该如何解决这种需求呢?就是你要使用事务的数据库连接对象,来实现数据的检索就可以了,如下面的代码就是OK的了。

        booldlg_OnDataSave(DataRow dr, DbTransaction trans)
{
string PlaneModel = dr["装备型号"].ToString();if (string.IsNullOrEmpty(PlaneModel)) return false;bool success = false;
PlaneModelInfo info
= BLLFactory<PlaneModel>.Instance.FindSingle(string.Format("PlaneModel='{0}'", PlaneModel), trans);if (info != null)
{
info.PlaneModel
=PlaneModel;
info.PlaneNote
= dr["保障特点"].ToString();
info.Demand
= dr["保障要求"].ToString();
info.Note
= dr["备注"].ToString();

info.Dept_ID
=Portal.gc.LoginInfo.Dept_ID;
success
= BLLFactory<PlaneModel>.Instance.Update(info, info.ID, trans);
}
else{
info
= newPlaneModelInfo();
info.PlaneModel
=PlaneModel;
info.PlaneNote
= dr["保障特点"].ToString();
info.Demand
= dr["保障要求"].ToString();
info.Note
= dr["备注"].ToString();

info.Dept_ID
=Portal.gc.LoginInfo.Dept_ID;
success
= BLLFactory<PlaneModel>.Instance.Insert(info, trans);
}
returnsuccess;
}

4、Winform开发框架的事务接口支持

基于此,我们很多查找的接口可能都会在事务中调用,需要重新构造我的框架基类接口了,把事务作为默认的对象参数,默认为NULL,调整我的基类,为所有的事务内操作提供支持,如数据访问接口层部分接口定义如下所示。

    /// <summary>
    ///数据访问层的接口/// </summary>
    public interface IBaseDAL<T> whereT : BaseEntity
{
#region 通用操作 /// <summary> ///获取表的所有记录数量/// </summary> /// <param name="trans">事务对象</param> /// <returns></returns> int GetRecordCount(DbTransaction trans = null);/// <summary> ///获取表的指定条件记录数量/// </summary> /// <param name="condition">条件语句</param> /// <param name="trans">事务对象</param> /// <returns></returns> int GetRecordCount(string condition, DbTransaction trans = null);/// <summary> ///根据condition条件,判断是否存在记录/// </summary> /// <param name="condition">查询的条件</param> /// <param name="trans">事务对象</param> /// <returns>如果存在返回True,否则False</returns> bool IsExistRecord(string condition, DbTransaction trans = null);/// <summary> ///查询数据库,检查是否存在指定键值的对象/// </summary> /// <param name="recordTable">Hashtable:键[key]为字段名;值[value]为字段对应的值</param> /// <param name="trans">事务对象</param> /// <returns>存在则返回<c>true</c>,否则为<c>false</c></returns> bool IsExistKey(Hashtable recordTable, DbTransaction trans = null);

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

BaseBLL业务基类的部分接口实现如下所示

    /// <summary>
    ///业务基类对象/// </summary>
    /// <typeparam name="T">业务对象类型</typeparam>
    public class BaseBLL<T> where T : BaseEntity, new()
{
............................
#region 对象添加、修改、查询接口 /// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="trans">事务对象</param> /// <returns>执行操作是否成功。</returns> public virtual bool Insert(T obj, DbTransaction trans = null)
{
CheckDAL();
returnbaseDal.Insert(obj, trans);
}
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回新增记录的自增长ID。</returns> public virtual int Insert2(T obj, DbTransaction trans = null)
{
returnbaseDal.Insert2(obj, trans);
}
/// <summary> ///更新对象属性到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="primaryKeyValue">主键的值</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> public virtual bool Update(T obj, object primaryKeyValue, DbTransaction trans = null)
{
CheckDAL();
returnbaseDal.Update(obj, primaryKeyValue, trans);
}
......................

基于事务性的调整,优化了整个基类接口和实现类的类库,以方便在框架中更好整合事务性操作的支持。

在平常的学习研究中,或者有时候因为开发项目的各种需要,会对界面设计进行各种方式的改良尝试,遇到好的经验实践,就一直在我自己的各种项目中沿用下去,本文主要分析我的一款病人资料管理系统-脊柱外科病人资料管理系统的系统界面设计,以及如何基于DevExpress控件的具体实现,从而为用户提供更好的界面效果体验。

1、系统主体界面设计

系统主体界面设计,遵循我的《
WInform开发框架
》的技术特点,使用基于Ribbon样式的多文档布局界面框架,通过在主工具栏里面对功能进行分类,实现更多的功能点排列,如下界面所示。

这样的界面效果适合大多数的Mis系统的布局,也较好呈现相关功能点。

2、查询功能的面板折叠

从上图我们看到,患者资料的查询条件比较多,已经占用了大部分的界面篇幅,但是我们常用的条件又需要放在在主面板里面,因此我通过在主界面中放置一个SplitContainerControl的控件,对屏幕控件进行分开,可以通过上下调整屏幕空间,并且还可以通过下图中间的一个三角符号,进行上面查询条件面板的隐藏,从而实现较好的屏幕调整。

在系统的运行状态界面中,单击面板的三角符号可以把界面折叠到上面,折叠后查询条件的面板全部隐藏起来,需要展开在单击三角符号即可,折叠效果如下所示。

以上界面效果,通过在界面里面放置一个SplitContainerControl控件,然后在上下面板放置好相关的控件,并设置SplitContainerControl其中的一些属性即可,属性设置如下所示。

3、字典控件的数据绑定

在一般的系统界面里面,我们往往需要从字典模块中提取数据,绑定到一些界面控件上,例如,下拉列表控件、单选组合控件,复选框组合控件等。下面介绍这些字典控件的数据绑定方面的案例代码。

我们看看这个界面的数据绑定代码。

        /// <summary>
        ///初始化字典列表内容/// </summary>
        private voidInitDictItem()
{
//初始化代码 txtBodyPart.BindDictItems("脊柱部位");
txtIsFirstTime.BindDictItems(
"首发复发");
txtInDiagnosis.BindDictItems(
"入院诊断");
txtDoctor.BindDictItems(
"主管医生");
txtTreatingPhysician.BindDictItems(
"经治医生");

txtLeaveDiagnosis.BindDictItems(
"最后诊断");
txtLeaveCondition.BindDictItems(
"出院情况");
txtSurgeonDoctor.BindDictItems(
"主刀医生");
txtSurgeryName.BindDictItems(
"手术名称");
txtSurgeryType.BindDictItems(
"手术方式");
txtPathology.BindDictItems(
"病理诊断");
txtIntervertebral.BindDictItems(
"椎间隙");
txtVertebral.BindDictItems(
"椎体");
}

这种方式是直接使用了扩展方法的方式定义控件的BindDictItems方法,把所有字典控件类型都扩展了该函数,以便实现更好的统一操作,ComboBoxEdit控件的扩展方法定义如下所示。

        /// <summary>
        ///设置下拉列表选中指定的值/// </summary>
        /// <param name="combo">下拉列表</param>
        /// <param name="value">指定的CListItem中的值</param>
        public static void SetComboBoxItem(this ComboBoxEdit combo, stringvalue)
{
for (int i = 0; i < combo.Properties.Items.Count; i++)
{
CListItem item
= combo.Properties.Items[i] asCListItem;if (item != null && item.Value ==value)
{
combo.SelectedIndex
=i;
}
}
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="combo">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> public static void BindDictItems(this ComboBoxEdit combo, stringdictTypeName)
{
BindDictItems(combo, dictTypeName,
null);
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="combo">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> /// <param name="defaultValue">控件默认值</param> public static void BindDictItems(this ComboBoxEdit combo, string dictTypeName, stringdefaultValue)
{
Dictionary
<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

BindDictItems(combo, itemList, defaultValue);
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="combo">下拉列表控件</param> /// <param name="itemList">数据字典列表</param> public static void BindDictItems(this ComboBoxEdit combo, List<CListItem>itemList)
{
BindDictItems(combo, itemList,
null);
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="combo">下拉列表控件</param> /// <param name="itemList">数据字典列表</param> /// <param name="defaultValue">控件默认值</param> public static void BindDictItems(this ComboBoxEdit combo, List<CListItem> itemList, stringdefaultValue)
{
combo.Properties.BeginUpdate();
//可以加快 combo.Properties.Items.Clear();
combo.Properties.Items.AddRange(itemList);
if (!string.IsNullOrEmpty(defaultValue))
{
combo.SetComboBoxItem(defaultValue);
}

combo.Properties.EndUpdate();
//可以加快 }

上面我定义了好几种方法的扩展,方便更好的对控件的字典数据的控制,如可以指定默认值,可以指定数据字典的类型名称即可绑定,或者可以指定一个列表集合即可。

在上面的最后一个函数代码里面,值得注意的是,我在其中添加注释说明的红色代码,这个可以很好提高界面的响应效率,利用BeginUpdate()和EndUpdate()的方法,可以极大提高数据绑定的效率,特别在数据源很大(如几百个以上的数据项目),速度可以提高好几倍,所以如果控件提供了这样的两个方法,我们在数据绑定的时候,最好坚持使用,如TreeView控件也提供了。

除了ComoboBoxEdit控件的数据源绑定操作,我们也可以通过以上相同的方法扩展其他数据源控件,如下面是RadioGroup控件的数据绑定扩展方法定义,也提供了上面类似的BindDictItems()方法的定义,直接可以绑定数据字典里面的内容。

        /// <summary>
        ///绑定单选框组为指定的数据字典列表/// </summary>
        /// <param name="radGroup">单选框组</param>
        /// <param name="dictTypeName">字典大类</param>
        /// <param name="defaultValue">控件默认值</param>
        public static void BindDictItems(this RadioGroup radGroup, string dictTypeName, stringdefaultValue)
{
Dictionary
<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
List
<RadioGroupItem> groupList = new List<RadioGroupItem>();foreach (string key indict.Keys)
{
groupList.Add(
newRadioGroupItem(dict[key], key));
}

radGroup.Properties.BeginUpdate();
//可以加快 radGroup.Properties.Items.Clear();
radGroup.Properties.Items.AddRange(groupList.ToArray());
//可以加快 if (!string.IsNullOrEmpty(defaultValue))
{
SetRaidioGroupItem(radGroup, defaultValue);
}

radGroup.Properties.EndUpdate();
//可以加快 }

4、附件管理分组-影像学资料管理

在很多病人资料管理模块中,都需要管理影像学资料,这个在我的Winfrom开发框架中,提供了一个附件管理的通用模块供重复使用,因此对这些操作并不会增加太多的工作量,统一使用即可,界面效果如下所示。

附件管理模块提供了分类的附件管理,图片缩略图预览和图片预览,多文件上传、下载、删除等附件操作,满足我们对附件管理的大多数要求,如下界面所示。更详细的通用附件管理模块的呈现和使用,可参考我之前的随笔《
Winform开发框架之附件管理应用
》,以及《
Winform开发框架之通用附件管理模块
》随笔。


5、特殊界面布局的调整

在录入数据的界面的设计中, 我一般倾向于使用LayoutControl控件实现布局功能,这个控件对我们排版各种控件非常方便。

不过由于该布局控件使用类似于表格单元格的模式,如下这种方式的布局是常规的控件排列方式(设计模式下界面),在LayoutControl控件中通过拉动排列控件即可很好实现。

但也是由于该控件的布局设计有点类似于表格行列的方式,因此也要设计更多样化的界面效果也有所约束,但总体来说,不失为一种非常棒的布局控件,它的一些缺点,可以通过一些特殊的操作进行避免。下面是我在其中的一个界面中使用该控件调整后得到的一个多样化一点的界面表达方式,有点类似于分组分类的方式,使得输入的内容更加易读。

这个调整好的界面布局的设计模式下的效果如下所示。

其中我们以“感觉系统”标签为例,我们在设计界面中增加一个EmptySpaceItem(注意不要拖入LableControl控件,这个标签控件不适合标题),调整到最佳位置后,需要设置好其相关的属性,才可以得到更好的界面效果,设计属性如下所示。

第二个的缩进标题的效果,也是通过类似的方式,设置控件的约束类型为Custom,然后调整它的最大最小宽度(Width),并且不设置文本为显示状态,从而控制标签的位置,实现占位符的效果。属性如下所示。

以上就是我在脊柱外科病人资料管理系统里面中,对程序界面设计分析和内在使用的一些经验总结,这些积累很大程度上,也是基于我的Winform开发框架中的优化和改善,总之,希望这些在实际项目中的一些案例使用,希望对大家有启发作用,期待大家在这方面的共同探讨。

在上篇随笔《
脊柱外科病人资料管理系统的界面设计分析
》中介绍了一些常用的界面设计方面的内容,本篇继续上一篇,介绍脊柱外科病人管理系统的JOA评分记录模块的界面设计以及实现方面的内容。

JOA(全称 Japanese Orthopaedic Association Scores for Assessment of Cervical Myelopathy),日本骨科学会(JOA)颈椎病疗效评定标准,用于在脊柱外科的术前术后,对患者身体状况进行量化,并制定相关的护理方案提供依据。JOA评分记录模块,是软件《脊柱外科病人资料管理系统》的一个亮点,能使外科医生或者护士,对患者的信息进行全面的记录和研究参考。

1、JOA评分记录模块的分析

JOA评分记录,有点类似于考题的方式,对各项内容进行分值的评估,每项记录的得分不同,汇总成一个总的得分,用于量化评估,它的分类大致如下所示。

从上面的文档截图可以看到,评分项目可以分为一个大类(如自觉症状),多个子项目(如下腰痛)这样的组织方式,然后我们需要记录每项的得分,以及所有项目累加的总分。

通过分拆,我们可以把一个记分的题目作为一个控件,每个题目它自己的得分明细可以动态从数据库获取即可,界面控件设计如下所示。

通过上面的分析,我们知道,一个项目大类(如自觉症状)包含了多个子项目(如下腰痛、步态等)这样的题目,也就是多个上面控件的实例,那么我们设计的项目大类控件界面如下所示。

以上的界面,其实就是在一个大的项目大类控件上组合多个子项目控件的效果。

然后,整个JOA评分的界面就是多个上面项目大类的组合了,组合成了一个JOA评分记录模块的控件效果,如下所示。

最后,我们在第一个文档截图里面看到,JOA评分分为两个方面,一个是术前的,一个是术后的,他们的评分界面完全一样,那么我们可以把它们用两个Tab界面进行分开处理,最后得到的运行界面如下所示。

2、JOA评分记录的逻辑实现

为了在界面呈现各项分数项目,我们需要在数据库设计一个表JOAItem,用来存储每个评分项目的内容,然后在界面中根据分类进行动态展示。

然后定义一个评分子项目的信息对象ControlItem,作为上面控件的信息对象,方便控件和数据库关联起来,记分或者存储明细到数据库中,代码如下所示。

    /// <summary>
    ///JOA评分项目明细/// </summary>
[DataContract]public classControlItem 
{
/// <summary> ///序号,从1开始/// </summary> [DataMember]public virtual int Seq { get; set; }/// <summary> ///分类名称/// </summary> [DataMember]public virtual string Category { get; set; }/// <summary> ///项目明细列表(包括项目得分等信息)/// </summary> [DataMember]public virtual List<JOAItemInfo> ItemList { get; set; }/// <summary> ///评分项目第一项得分(默认得分)/// </summary> public virtual decimalDefaultScore
{
get{if (ItemList == null || ItemList.Count == 0)
{
return0M;
}
else{return ItemList[0].Score;
}
}
}
/// <summary> ///默认构造函数/// </summary> publicControlItem()
{
ItemList
= new List<JOAItemInfo>();
}
/// <summary> ///参数构造函数/// </summary> /// <param name="seq">序号</param> /// <param name="category">分类名称</param> /// <param name="itemList">项目明细列表</param> public ControlItem(int seq, string category, List<JOAItemInfo> itemList) : this()
{
this.Seq =seq;this.Category =category;this.ItemList =itemList;
}
}

在上面的评分控件中,我们整合以上的信息对象作为一个整体,部分代码如下所示。

    public partial classScoreItemControl : DevExpress.XtraEditors.XtraUserControl
{
/// <summary> ///单项的序号,从1开始/// </summary> public int Seq = 1;/// <summary> ///控件绑定的信息/// </summary> public ControlItem ControlItem { get;set; }/// <summary> ///单项的得分/// </summary> public decimal Score { get; set; }/// <summary> ///处理分数变化后的事件触发/// </summary> public eventScoreChangedHandler OnScoreChanged;

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

当我们在评分控件中指定了ControlItem信息后,在控件的Load事件里面,将会动态绑定评分的项目明细,代码如下所示。

        private void ScoreItemControl_Load(objectsender, EventArgs e)
{
if (ControlItem != null && !this.DesignMode)
{
this.lblItemIndex.Text =ControlItem.Seq.ToString();this.layoutItem.Text =ControlItem.Category;

List
<CListItem> itemList = new List<CListItem>();foreach (JOAItemInfo info inControlItem.ItemList)
{
itemList.Add(
newCListItem(info.ItemDetail, info.Score.ToString()));
}

radItemGroup.BindDictItems(itemList);
}
if (radItemGroup.Properties.Items.Count > 0)
{
this.radItemGroup.SelectedIndex = 0;
}
}

除了ControlItem信息对象的绑定,我们还注意到上面的
OnScoreChanged
事件,它就是为了我们在整个控件中实现分数动态变化的一个事件,这样我们变化任何一个评分项目信息,单项分数和总分都会重新计算一次的事件。

        /// <summary>
        ///处理分数变化后的事件触发/// </summary>
        public virtual void ProcessScoreChanged(int seq, decimalscore)
{
if (OnScoreChanged != null)
{
OnScoreChanged(seq, score);
}
}
private void radItemGroup_SelectedIndexChanged(objectsender, EventArgs e)
{
this.lblItemScore.Text = string.Format("得分({0})分", this.radItemGroup.EditValue);decimal result = 0;if (decimal.TryParse(this.radItemGroup.EditValue.ToString(), outresult))
{
this.Score =result;
ProcessScoreChanged(Seq, result);
}
}

以上代码逻辑就是最小评分控件的一个具体的实现,完成以上这些,还需要完成一个评分项目大类的具体逻辑,它的操作方式和上面差不多,也是引入一个信息对象集合作为背后得分计算的逻辑。

定义的信息对象ProjectItem代码如下所示。

   /// <summary>
   ///JOA评分项目信息/// </summary>
[DataContract]public classProjectItem
{
/// <summary> ///序号,从1开始/// </summary> [DataMember]public virtual int Seq { get; set; }/// <summary> ///项目名称/// </summary> [DataMember]public virtual string Project { get; set; }/// <summary> ///项目明细列表/// </summary> [DataMember]public virtual List<ControlItem> ItemList { get; set; }/// <summary> ///默认构造函数/// </summary> publicProjectItem()
{
ItemList
= new List<ControlItem>();
}
/// <summary> ///评分项目第一项得分总和(默认得分)/// </summary> public virtual decimalDefaultScore
{
get{if (ItemList == null || ItemList.Count == 0)
{
return0M;
}
else{decimal total =0M;foreach (ControlItem item inItemList)
{
total
+=item.DefaultScore;
}
returntotal;
}
}
}
/// <summary> ///参数化构造函数/// </summary> /// <param name="seq"></param> /// <param name="project"></param> /// <param name="itemList"></param> public ProjectItem(int seq, string project, List<ControlItem> itemList) : this()
{
this.Seq =seq;this.Project =project;this.ItemList =itemList;
}
}

然后该控件的逻辑代码就是结合这个信息对象以及控件的一些事件进行处理了,控件的部分代码如下所示。

    public partial classScoreProjectControl : DevExpress.XtraEditors.XtraUserControl
{
/// <summary> ///项目大类的序号,从1开始/// </summary> public int ProjectSeq = 1;/// <summary> ///项目大类的信息/// </summary> public ProjectItem ProjectItem { get; set; }/// <summary> ///处理分数变化后的事件触发/// </summary> public eventScoreChangedHandler OnScoreChanged;private Dictionary<int, decimal> scoreList = new Dictionary<int, decimal>();//记录单项的得分列表 ...................

由于每项评分子项目是单独的评分控件,控件的布局采用了FlowLayout布局呈现方式,因此在控件的Load事件代码如下所示。

        private void ScoreProjectControl_Load(objectsender, EventArgs e)
{
if (ProjectItem != null && !this.DesignMode)
{
this.lblProject.Text = string.Format("{0}、{1}", ProjectItem.Seq, ProjectItem.Project);this.flowLayoutPanel1.Controls.Clear();int index = 1;foreach (ControlItem item inProjectItem.ItemList)
{
ScoreItemControl control
= newScoreItemControl();
control.ControlItem
=item;
control.Seq
= index++;
control.OnScoreChanged
+= newScoreChangedHandler(control_OnScoreChanged);this.flowLayoutPanel1.Controls.Add(control);
}
}
}

同样整个控件也有一个
OnScoreChanged
的事件,我们看到,这个和上面介绍的事件操作方式类似,都是一级负责一级的分数处理,具体代码如下所示。

        void control_OnScoreChanged(int seq, decimalscore)
{
if(!scoreList.ContainsKey(seq))
{
scoreList.Add(seq, score);
}
else{
scoreList[seq]
=score;
}
//项目的总分变化 ProcessScoreChanged(ProjectSeq, this.TotalScores);
}

以上的控件实现逻辑一步步递推,就能很好实现评分项目的动态呈现,以及控件评分分数的动态变化和总分记录保存,由于涉及的实现细节还比较多,一篇随笔介绍内容太多显得累赘,但是其他控件的实现逻辑和上面的操作方式差不多,在这里就不在一一赘述了,本文主要提供一个这种的实现思路进行JOA评分记录模块的实现,进一步拓展,可以把它应用到考试题目上 ,可以作为动态抽取题目,然后记录测试者的题目选择信息,最后把测试者的答案和标准答案对比得出用户的总分,这样就完成了试题的动态测试案例了。