2023年2月

在上篇随笔《
Winform开发框架之框架演化
》中介绍了几种Winform开发框架,其中有对于离线式WCF开发框架的介绍,
离线式的WCF开发框架 ,就是结合了传统Winform开发框架的数据访问方式,又利用了WCF分布式数据获取的特点,使得数据可以离线使用,在一种业务要求集中化,又要求不影响正常业务操作的应用系统场景下比较适合。本文主要介绍如何利用我的Winform开发框架的整体思路,实现WCF开发框架的离线式的数据上传、更新的同步操作。

其实目前企业集中化管理,这种模式要求很多,如一些加盟店的情况,需要独立运行,有可以对一些总店关键数据进行提交或者下载,如客户信息等。这种情况下,就要求我们开发者提供适合应用场景的开发框架进行支持。
离线式的WCF开发框架,一个特点就是基本上显示,以及保存等操作数据库的数据,都是本地的数据库,不是远端的服务器数据库,这样,就需要记录所有发生变更的数据库操作,包括写入,删除、修改等,以便在网络畅通的情况下,可以上传数据到服务器上面。

下面我们来分析下这种离线式的WCF开发框架,需要做哪些准备工作,来实现框架的支撑。

1、数据库表记录ID定义唯一性。

这个是常见分布式系统的要求了,在一些普通的Winform程序的数据库中也比较常见,之所以把它作为第一条,虽然简单,但是很必要,因为需要避免分布式的客户端和服务端的数据冲突问题,特别在多个客户端的情况下,对数据的唯一性要有好的控制性。

所以这也要求基础的框架基类,能够提供对整形、字符型的主键ID的操作兼容性,这在我的Winform开发框架中,支持是比较好的。

2、多数据库支持

在分布式的环境下,和服务端的环境不同,部署程序要求越简单越好,太复杂的话,增加客户端的使用的难度,会极大提高维护的成本,因此,一般客户端会选用适应性比较好,又免安装的数据库,如Sqlite就是一个很好的单机版数据库,还有Access也是很不错的,当然还有其他的一些数据库,不过我觉得Sqlite和Access是比较好的备选方案。服务器端的数据库,则看业务支持和响应程度来决定,可以从一些对性能支持比较好的数据库中选型,如大型一点的,可选择Oracle来做,其他的可以选择SqlServer、MySql等数据库。虽然这些数据库部署比较麻烦一点,不过反正只有一台服务器需要这种安装部署,所以工作难度及工作量不会很大。

对多数据库的支持,也要求我们的开发框架能够很好兼容,最好在数据库操作层可以通过配置方式进行切换,即使数据库变化为其他类型,也不需要改变整体的框架布局,甚至不用变化代码即可实现自由切换,如数据库框架可以设置如下。


对于上面几种数据库的支持,一般来说,需要增加不同数据库类型的BaseDAL,由于每个不同数据库都需要拥有一个BaseDAL,那么很多相同的操作代码就会发生冗余,因为大多数数据库的基础操作是一样的,只有一部分比较特别,需要进行个性化处理,因此对数据访问层进行优化设计,得到下面的设计图,如下所示。

经过框架抽象,这个BaseDAL类代码很少,基本上通用的数据库操作,已经放到了AbStractBaseDAL超级基类进行封装,即使对于一些不同数据库操作不同,我们也尽可能抽象放到上面基类了,BaseDAL只需要实现一些特殊的操作即可。

3、分布式客户端数据上传设计

由于分布式,离线式的框架设计,要求我们客户端自行记录数据的变化情况,包括新增数据、修改数据和删除数据,这样不用每次同步的时候,把所有的数据库记录都遍历一次,然后和服务器记录进行比较。这种记录方式,可以极大提高客户端数据上传的性能和快捷性。因为我们对于很多表及记录的数据库,可能每次更新的只是一小部分,这样设计,有利于我们更好地额处理客户端数据上传。

例如,下面的表,就是对于一个客户端上传记录表的设计,其中Dept_ID是用来记录不同部门的表示,基本上每个客户端,都有自己的一个部门编号,防止数据发生冲突,也方便服务器端的数据进行归类查询。

下面是一些实际业务产生的数据记录,我们记录部门ID、表名(发生变化)、对应记录的ID(GUID)、修改用户、修改时间等信息。

另外,我们还可以结合系统来记录用户登录信息、用户对记录修改的日志,以便我们对一些关键操作进行审计需要。数据库设计如下所示。

4、数据修改记录自动记录

对于上面的to_upload表,我们是把客户端修改的数据记录信息,记录到表里面去,但是这些肯定是后台自动记录的,而且这个操作是放到基类比较合适,否则每次调用,不太方便,也比较冗余。

放到基类的操作,我们需要设计一下,否则所有的表都会记录,不管需不需要,这样不可以的。

首先我们在基类BaseDAL(对Sqlite的数据库基类),增加一个变量来记录是否数据库访问基类,需要记录数据库变化信息。

protected bool IsLogToUpoad = false; //表示是否记录变化

对于具体业务对象的数据访问,我的Winform开发框架都有提供一个对应的类来进行操作。

    /// <summary>
    ///药品信息/// </summary>
    public class DrugDetail : BaseDAL<DrugDetailInfo>, IDrugDetail
{
#region 对象实例及构造函数 public staticDrugDetail Instance
{
get{return newDrugDetail();
}
}
public DrugDetail() : base("M_DrugDetail","ID")
{
this.IsLogToUpoad = true;this.sortField = "EditTime";this.isDescending = true;
}
#endregion
..........................................

为了要实现自动记录数据库变化信息,我们需要在BaseDAL里面对插入、修改、删除的操作进行特别的处理,重载基类的操作,增加相应的处理即可,如下代码所示。

        private void AddToUpload(string id, string targetTable, System.Data.Common.DbTransaction trans, intuploadType)
{
AppConfig config
= newAppConfig();

ToUploadInfo info
= newToUploadInfo();
info.EditTime
=DateTime.Now;
info.RecordId
=id;
info.TableName
=targetTable;
info.UploadType
=uploadType;
info.Dept_ID
= config.AppConfigGet("Dept_ID"); ;
info.User_ID
= config.AppConfigGet("User_ID");
Hashtable uploadHash
=GetHashByObject(info);base.Insert(uploadHash, "SS_ToUpload", trans);
}
public override bool PrivateUpdate(object id, Hashtable recordField, stringtargetTable, DbTransaction trans)
{
bool result = base.PrivateUpdate(id, recordField, targetTable, trans);if (result &&IsLogToUpoad)
{
AddToUpload(recordField[
"ID"].ToString(), targetTable, trans, 1);
}
returnresult;
}
public override bool Insert(System.Collections.Hashtable recordField, stringtargetTable, System.Data.Common.DbTransaction trans)
{
bool result = base.Insert(recordField, targetTable, trans);if (result &&IsLogToUpoad)
{
AddToUpload(recordField[
"ID"].ToString(), targetTable, trans, 0);
}
returnresult;
}

由于是上面的PrivateUpdate和Inser方法,是所有派生的更新、插入接口的最原始的函数,所有其他相关函数都会调用这两个的基础函数, 这样就基本实现了数据库记录的变化记录了。

5、分布式客户端数据和服务器端的同步

为了和服务器实现同步,需要实现变化记录的上传和服务器修改数据的下载两个方向的工作。

变化记录的上传从操作,就是遍历to_upload里面的记录,把它更新到服务器上即可。

        /// <summary>
        ///把本地变化的数据记录,同步到服务器上/// </summary>
        /// <returns></returns>
        public boolSyncAll()
{
List
<ToUploadInfo> toList = BLLFactory<ToUpload>.Instance.GetAll();int i = 1;int total =toList.Count;bool success = false;foreach (ToUploadInfo toInfo intoList)
{
switch(toInfo.TableName.ToLower())
{
                    case "m_drugusedetail":
success
=DealDrugUseDetail(toInfo);if (!success) return false;break;
.......................................//其他操作,利用服务器代理对象,实现各个表的数据上传}#region 显示进度等处理
                string tips = string.Format("正在同步表 {0}...", toInfo.TableName);int step = 0;if (total > 0)
{
step
= Convert.ToInt32((100.0 / (1.0 * total)) *i);
}
if (OnDataDealed != null)
{
OnDataDealed(step, tips);
}
i
++;if (toInfo == null || string.IsNullOrEmpty(toInfo.TableName))
{
continue;
}
#endregion}#region 同步系统关键数据 if(success)
{
//部门 SynAllDept();//已上传数据表同步 DealUploaded();

SyncBasicData();
}
#endregion return true;
}

为了实现数据上传操作,我们把逻辑封装在一个函数里面,这样方便管理,也方便阅读。

        private boolDealDrugUseDetail(ToUploadInfo toInfo)
{
bool success = false;
DrugUseDetailInfo objInfo
= BLLFactory<DrugUseDetail>.Instance.FindByID(toInfo.RecordId);if (objInfo != null && objInfo.Dept_ID ==Portal.gc.LoginInfo.Dept_ID)
{
new DrugUseDetailServiceClient().Using(client =>{
success
=client.InsertUpdate(objInfo, objInfo.ID);
});
if(success)
{
RemoveToUploadInfo(toInfo);
}
}
returnsuccess;
}

数据同步的下载操作,其实也不难,就是把数据对应的记录下载下来进行判断。

        private voidDealInHospital()
{
List
<InHospitalInfo> list = new List<InHospitalInfo>();new InHospitalServiceClient().Using(client =>{
list
=client.Find(conditionPilotDept);
});
int i = 1;int total =list.Count;foreach (InHospitalInfo info inlist)
{
BLLFactory
<InHospital>.Instance.InsertUpdate(info, info.ID);
ShowProgress(total, i
++, "住院信息");
}
}

在Web开发中,有很多可以上传的组件模块,利用HTML的File控件的上传也是一种办法,不过这种方式,需要处理的细节比较多,而且只能支持单文件的操作。在目前Web开发中用的比较多的,可能uploadify(参考
http://www.uploadify.com/
)也算一个吧,不过这个版本一直在变化,他们的脚本调用也有很大的不同,甚至调用及参数都一直在变化,很早的时候,那个Flash的按钮文字还没法变化,本篇随笔主要根据项目实际,介绍一下3.1版本的uploadify的控件使用,这版本目前还是最新的,因此对我们做Web开发来说,有一定的参考性。

这个控件有很多参数控制,以及事件的处理响应,相对来说也比较好用。参数控制可以控制上传文件多选、文件类型、文件大小、文件数量、检查文件是否存在,以及一些按钮参数的控制,如文字、高度、宽度等,对提交文件成功与否、完成操作、取消、停止上传等等都有控制,他们的帮助文档也写得比较完善,不过就是各个版本的方法参数完全不同了,但控件是一个好控件。

控件的使用首先要加入必备的脚本类库,由于该控件是利用了Jquery的功能,因此还需要应用Jquery脚本文件,如下所示。

    <script src="http://www.cnblogs.com/JQuery/jquery-1.8.0.min.js" type="text/javascript"></script>
    <script src="http://www.cnblogs.com/JQueryTools/uploadify/jquery.uploadify-3.1.min.js" type="text/javascript"></script>
    <link href="http://www.cnblogs.com/JQueryTools/uploadify/uploadify.css" rel="stylesheet" type="text/css" />

配置控件的一些参数,以及相应的处理事件,如下所示。

    <script language="javascript" type="text/javascript">$(function () {var guid = '<%=Request["guid"] %>';var type = '<%=Request["type"] %>';if (guid == null || guid == "") {
guid
=newGuid();
}
if (type != null) {
type
= type + '/';
}

$(
'#file_upload').uploadify({'swf': 'uploadify.swf', //FLash文件路径 'buttonText': '浏 览', //按钮文本 'uploader': 'uploadhandler.ashx?guid=' + guid, //处理ASHX页面 'formData' : { 'folder' : 'picture' }, //传参数 'queueID': 'fileQueue', //队列的ID 'queueSizeLimit': 10, //队列最多可上传文件数量,默认为999 'auto': false, //选择文件后是否自动上传,默认为true 'multi': true, //是否为多选,默认为true 'removeCompleted': true, //是否完成后移除序列,默认为true 'fileSizeLimit': '10MB', //单个文件大小,0为无限制,可接受KB,MB,GB等单位的字符串值 'fileTypeDesc': 'Image Files', //文件描述 'fileTypeExts': '*.gif; *.jpg; *.png; *.bmp', //上传的文件后缀过滤器 'onQueueComplete': function (event, data) { //所有队列完成后事件//ShowUpFiles(guid, type, show_div); alert("上传完毕!");
},
'onUploadError': function (event, queueId, fileObj, errorObj) {
alert(errorObj.type
+ "" +errorObj.info);
}
});
});

function newGuid() {
var guid = "";for (var i = 1; i <= 32; i++){var n = Math.floor(Math.random()*16.0).toString(16);
guid
+=n;if((i==8)||(i==12)||(i==16)||(i==20))
guid
+= "-";
}
returnguid;
}
</script>

再次提一下,这个控件不要参考网上其他的一些说明,否则可能参数及用法不正确,一定要找到对应版本的说明(本篇指的是3.1.1),最好参考该版本的在线文档。

上面的参数,我基本上都给了注释了,还有一些不是很重要的参数,这里没有列出来,需要可以参考在线文档吧。

值得提到的是,这个版本可以修改Flash里面的文字,非常棒,很讨厌以前的那个默认Browse的英文,虽然以前替代图片可以修改文字,但是还是不太好用。这个直接修改文字,非常好。

值得注意的是uploader参数,这个是我们ashx的后台处理程序,就是控件提交文件给那个页面进行保存处理,添加数据库记录等操作。

页面代码使用很简单,如下所示

<bodystyle="margin-left:10px; margin-top:10px">
    <formid="form1"runat="server"enctype="multipart/form-data">
    <divid="fileQueue"class="fileQueue"></div>

    <div>
    <inputtype="file"name="file_upload"id="file_upload" />
        <p>
            <inputtype="button"class="shortbutton"id="btnUpload"onclick="javascript:$('#file_upload').uploadify('upload','*')"value="上传" />
            &nbsp;&nbsp;&nbsp;&nbsp;
            <inputtype="button"class="shortbutton"id="btnCancelUpload"onclick="javascript:$('#file_upload').uploadify('cancel')"value="取消" />
        </p>
        <divid="div_show_files"></div>
    </div>
    </form>
</body>

关键是后台上传文件的保存操作了,asp.net一般采用ashx的处理页面来处理。

    /// <summary>
    ///文件上传后台处理页面/// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo
=WsiProfiles.BasicProfile1_1)]public classUploadHandler : IHttpHandler
{
public voidProcessRequest(HttpContext context)
{
context.Response.ContentType
= "text/plain";
context.Response.Charset
= "utf-8";try{string guid = context.Request.QueryString["guid"];string folder = context.Request["folder"];//LogTextHelper.Info(folder); HttpPostedFile file= context.Request.Files["Filedata"];if (file != null)
{
string oldFileName = file.FileName;//原文件名 int size = file.ContentLength;//附件大小 string extenstion = oldFileName.Substring(oldFileName.LastIndexOf(".") + 1);//后缀名 string newFileName = GetNewFileName(oldFileName);//生成新文件名//LogTextHelper.Info(newFileName); #region 上传到远程服务器 //FileServerManage fsw = new FileServerManage();//string uploadFilePath = "/" + newFileName;//if (!string.IsNullOrEmpty(folder))//{//uploadFilePath = string.Format("/{0}/{1}", folder, newFileName);//}//bool uploaded = fsw.UploadFile(file.InputStream, "/" + folder + "/" + newFileName); #endregion #region 本地服务器上传AppConfig config= newAppConfig();string uploadFiles = config.AppConfigGet("uploadFiles");if (string.IsNullOrEmpty(uploadFiles))
{
uploadFiles
= "uploadFiles";
}
if (!string.IsNullOrEmpty(folder))
{
uploadFiles
=Path.Combine(uploadFiles, folder);
}
string uploadPath = Path.Combine(HttpContext.Current.Server.MapPath("/"), uploadFiles);if (!Directory.Exists(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}
string newFilePath =Path.Combine(uploadPath, newFileName);
LogTextHelper.Info(newFilePath);
file.SaveAs(newFilePath);
bool uploaded =File.Exists(newFilePath);#endregion if(uploaded)
{
#region 文件保存成功后,写入附件的数据库记录 //AttachmentInfo attachmentInfo = new AttachmentInfo();//attachmentInfo.EditorTime = DateTime.Now;//attachmentInfo.FileExtend = extenstion;//attachmentInfo.FileName = folader + "/" + newFileName;//attachmentInfo.OldFileName = oldFileName;//attachmentInfo.Size = size;//attachmentInfo.Guid = guid;//BLLFactory<Attachment>.Instance.Insert(attachmentInfo); #endregion}
}
else{
LogTextHelper.Error(
"上传文件失败");
}
}
catch(Exception ex)
{
LogTextHelper.Error(
"上传文件失败", ex);throw;
}
}
/// <summary> ///获取新的名称 比如:aa.jpg转化为aa(20090504).jpg/// </summary> /// <param name="fileName">文件名称[aa.jpg]</param> /// <returns>新的文件名称</returns> public static string GetNewFileName(stringfileName)
{
if (string.IsNullOrEmpty(fileName))return string.Empty;//文件后缀名 string extenstion = fileName.Substring(fileName.LastIndexOf(".") + 1);string name = fileName.Substring(0, fileName.LastIndexOf(".")) + "(" + DateTime.Now.ToFileTime() + ")";string newFileName = name + "." +extenstion;returnnewFileName;
}
public boolIsReusable
{
get{return false;
}
}
}

执行例子的上传操作,我们会提示上传成功的操作,对应的目录下,会有相应的文件写入了。

以上就是这个批量上传文件控件uploadify的使用说明,供大家学习参考。

Web开发中,目前由于Jquery的大行其道,因此很多弹出对话框,都用到了Jquery技术,反而原始的弹出对话框的方式较为少用了。不过基于JQuery的方式实现对话框窗口弹出,也有很多控件可以利用,由于工作需要及业余兴趣所至,我比较了近10种的对话框控件,其中发现有一些做得很好的,除了功能强大,而且也支持多种皮肤样式,甚至有些对话框的居中都考虑到了,细节决定体验,有些真的不错。

1、原始的弹出对话框实现(弹出窗口也可以)

我们知道,以前在没有应用其他javascript库(例如各种类型的Jquery库)的时候,一般是通过window.open或者window.showModalDialog来弹出非模态或者模态的对话框的,如下脚本所示。

functionOpenWin( sURL , sFeatures )
{
window.open( sURL ,
null , sFeatures , null)//window.open("Sample.htm",null,"height=200,width=400,status=yes,toolbar=no,menubar=no,location=no"); //window.open( [sURL] [, sName] [, sFeatures] [, bReplace]) //sName{_blank; _media; _parent; _search; _self; _top} //sFeatures{channelmode; directories; fullscreen; height; left; location; menubar; resizable; scrollbars; status; titlebar; toolbar; top; width} }functionShowWin( sURL , sFeatures )
{
if(sFeatures == null || sFeatures == ""){
sFeatures
= 'dialogHeight:300px;dialogWidth:850px;status:no;scroll:yes;resizable:yes;help:no;center:yes;';
}
var returnValue= window.showModalDialog( sURL , null, sFeatures)//window.showModalDialog("Sample.htm",null,"dialogHeight:591px;dialogWidth:650px;") //window.showModalDialog([sURL] [, vArguments] [, sFeatures]) //sFeatures{dialogHeight; dialogLeft; dialogTop; dialogWidth; center; dialogHide; edge; help; resizable; scroll; status; unadorned} if(returnValue !=undefined)
{
returnreturnValue;
}
else{return "";
}
}

这种是原始方式,好处坏处大家都明白,就不说了。

还有一种基于Jquery的原始弹出窗口方式,由于我一般使用的是easy-ui作为界面的基础组件,因此基于easy-ui的原始弹出窗口方式,只能是弹出内部的HTML层内容,对于弹出独立的页面这种方式支持不够好。

    <scripttype="text/javascript"> 

    functioninitDialog(divname) {vardlg=jQuery(divname).dialog({
draggable:
true,
resizable:
true,
closed:
true,
show:
'Transfer',
hide:
'Transfer',
autoOpen:
false,
width:
500,
height:
250,
minHeight:
10,
minwidth:
10});
dlg.parent().appendTo(jQuery(
"form:first"));
};
functionclose(divname) {
$(divname).dialog(
'close');
}
</script>

2、Popup的弹出式对话框

这个popup控件弹出的对话框,它好像把弹出的子页面放到了父窗口里面了,感觉是一体化的,所以操作父窗口的对话框也可以,非常方便。

这个popup控件,比较容易使用,不过不好的地方就是不兼容其他浏览器如Chrome等,其他浏览器显示层是错位的,不好看,而且样式也相对比较简单一点了。

但由于在弹出窗口中可以很好操作父窗口的脚本以及使用方便,在我以前的一些项目里面,用的还算比较多。

3、AsyncBox的弹出对话框

这个控件感觉做的很不错,而且提供了Chrome、QQBrowser、Ext、ZCMS四种不错的窗体皮肤,适应多种需要,通过修改它们的皮肤图片,还可以定义适合自己项目的样式皮肤,如我修改定义了一种黄色调的样式例子。

不过在使用过程中,发现这个控件,对有些脚本或者Jquery控件有冲突,具体原因未明,总之发现了和基于Jquery的ZTree运行不正常,还有一些Jquery组件使用也不正常,在我的Web权限管理系统中,本来想用这个作为弹出窗口显示一些设置信息的,发现zTree的Node选中值获取不了,也就相当于失效了,非常郁闷,弄了很久,没有找到具体原因。还有另外一个问题,就是这个控件的作者不知道什么原因,好像停止了对该控件的开发了,连它的官网也下架了。

不过对于弹出窗口的使用来说,该控件已经很不错了,提供了多种调用模块,而我一般倾向于弹出另外一个页面这种方式,当然它也支持弹出页面内部的层或者HTML代码等等方式。

4、artDialog弹出对话框

这个artDialog弹出对话框组件,是我觉得相当好的一款了,除了支持多种浏览器,而且提供的界面效果更多,目前的版本是V4.1.6,好像还有一个V5.0的Beta版本(
https://github.com/aui/artDialog
),但是V5.0的就调整了不支持通过art.dialog.open方式弹出独立Web页面的方式了,要实现弹出独立页面,需要使用Iframe的代码,效果就差了一些,所以我倾向于V4.1.6。

该控件支持自动计算居中位置,我们只需要指定对话框的大小即可,当然它很好支持页面内的层内容的弹出显示,不过我更关注的是独立页面的弹出对话框显示,我在具体的Web权限管理系统中应用的效果如下所示(结合了ZTree控件,运行正常)。

这个控件提供了很多参数以及方法,对实现调用非常强大。

    <scriptsrc="http://www.cnblogs.com/JQueryTools/artDialog/artDialog.source.js?skin=blue"type="text/javascript"></script>
    <scriptsrc="http://www.cnblogs.com/JQueryTools/artDialog/plugins/iframeTools.source.js"type="text/javascript"></script>

首先封装一个通用的Javascript函数

functionShowArtDlg(title, url, width, height, lock) 
{
if (width == null || width == "") {
width
= '90%';
}
if (!width.indexOf('px') && !width.indexOf('%')) {
width
= width + 'px';
}
if (width.indexOf('px') < 0 && width.indexOf('%') < 0) {
width
= width + 'px';
}
if (height == null || height == "") {
height
= '90%'}if (height.indexOf('px') < 0 && height.indexOf('%') < 0) {
height
= height + 'px';
}
if (lock == null || lock == "") {
lock
= false;
}
art.dialog.open(url, { height: height, width: width, title: title, lock: lock },
false); //打开子窗体 }

页面里面调用的代码如下所示。

                    <tralign="right">
                        <td>
                            <ahref="#"class="easyui-linkbutton"iconcls="icon-edit"id="btnAddUser"onclick="ShowArtDlg('编辑-包含用户', 'EditTree.aspx?type=user&ouid=' + $('#txtID').val(), '360px', '500px')"runat="server">编辑</a>&nbsp;&nbsp;
                            <ahref="#"class="easyui-linkbutton"iconcls="icon-remove"id="btnDeleteUser"onclick="deleteUser()"runat="server">移除</a>
                        </td>
                    </tr>

如果你用5.0+的ArtDialog,调整了不支持通过art.dialog.open方式弹出独立Web页面的方式,但还是可以通过以下的方法去弹出独立页面。

art.dialog({title : "选择***",
cancel : true,
width : 600,
padding : '5px',
content : '
<iframesrc="test.aspx"id="test"name="test"height="400"width="600"frameborder="0"></iframe>',
ok : function(){
...
}
});

5、还有一些弹出窗口控件,还支持窗口的最大化操作,这个功能还是不错的。

在做Winform项目的时候,一直有一个梦想,就是希望把所有的组件模块组合即可组装成一个完整的项目系统(或者至少可以大部分完成)。这即使是梦想,我也一直为之奋斗,每前进一步,我们离梦想就靠近一步。因此,本着这个梦想,我一路走来,开发整理了一系列的组件模块,包括底层的公用类库、Winform分页控件、通用的适应多数据库的查询组件,以及相对高层次一点的组件模块:通用权限管理系统、通用字典管理模块、通用程序自动更新模块、以及本篇随笔介绍的
通用附件管理模块
,当然还会有更多的组件模块会吸引我继续朝着梦想前进。除了这些,为了提高开发效率,从设计好的数据库,直接生成项目代码,从05年开始至今,一直完善我的代码自动生成工具Database2Sharp。下面的附图,是我对于目前Winform开发框架以及将来的发展规划,朝着梦想前进,用博客园记录我的前进轨迹以及感想。

本片随笔,还是落地生根,继续介绍我的Winform开发框架中的一环,通用附件管理模块。该模块其实是很通用的一个模块,例如我们的一些日常记录,可能会伴随着有图片、文档等的附件管理,如果为每个业务对象都做一个附件管理,或者每次开发系统都重新做,那么效率肯定没有直接采用通用的附件管理那么方便快捷了。而且在日益增多的项目管理中,我们不需要维护一大堆相同或者类似的代码,而且我的组件都是内置支持多数据库的,对不同的数据库,只需要适当配置即可正常使用,对于组件化的基础性平台以及支持多数据库等方面,特别是项目管理等方面,颇具争议及传奇色彩的园友,
吉日嘎拉
在文章《
分享从带头拼死拼活开发软件项目到不去现场异地坐镇远程遥控照样可以把上海的软件项目管理好
》就有很好的阐述,其实他这些总结很实在,有着很好的基础基类(自己构建的或者购买的)总比从头来过的强,术业有专攻,更是厚积薄发积累的体现。

1、多数据库支持模块

我的一贯做法,就是所有的模块,为了应付未知的项目需求,都做成多数据库支持的,虽然看似麻烦了一点,但是由于我提炼的框架,数据库访问类都高度抽象化及完好的封装,因此即使增加多种数据库的支持,其实需要调整的地方极少。

对于上面几种数据库的支持,一般来说,需要增加不同数据库类型的BaseDAL,由于每个不同数据库都需要拥有一个BaseDAL,那么很多相同的操作代码就会发生冗余,因为大多数数据库的基础操作是一样的,只有一部分比较特别,需要进行个性化处理,因此对数据访问层进行优化设计,得到下面的设计图,如下所示。

经过框架抽象,这个BaseDAL类代码很少,基本上通用的数据库操作,已经放到了AbStractBaseDAL超级基类进行封装,即使对于一些不同数据库操作不同,我们也尽可能抽象放到上面基类了,BaseDAL只需要实现一些特殊的操作即可。

2、模块控件化,最大化降低开发工作

为了减少重复开发,要求控件尽可能考虑实际的需求情景。一般来说,我们在数据编辑界面,会有两个需求,一个是管理与数据记录对应的附件列表,一个是维护自己的附件信息,下面对这两个需求进行描述和讲解。

1)管理与数据记录对应的附件列表

首先我们创建一个独立的控件,用于放到编辑数据记录窗口里面,如下所示。

这样在项目中集成(如数据编辑窗口),直接拖动这个控件到界面中,运行就可以看到下面的效果了。

由于一般创建记录的时候,给他指定一个GUID的附件组ID,这样我们在数据记录保存前,我们就可以上传附件了,如下所示。

而且在这个过程中,可以随时查看自己在该记录中已经上传的附件。

如果附件不够,可以随时启动上传操作,附件支持多选文件,然后一次性,启动后台线程操作方式,把文件上传及附件记录保存到数据库,界面如下所示。



2)维护自己的附件信息

有时候,我们需要管理自己的个人附件,还需要知道自己在业务模块中上传过哪些附件,这两个是比较常见的场景,这样我们开发一个界面来管理查看这两类附件,就可以满足大多数的要求了,如下所示。

因为个人附件或者业务附件都可能比较多,甚至随着业务的增长,数量可能激增,那么分页就很有必要,如上图下发就是利用我的分页控件模块(纯分页控件模块,不含列表),这个分页控件集合是我博客介绍得比较多的一个控件来的,而且这个是其中之一的纯分页控件,可以适用于所有分页的场景,而不仅仅是用来显示二维表这么简单。当然,这个纯分页控件的使用也是简单易用的,可以用在各种需要分页显示的场合中,这个ListView就是其中之一,还可以用在图片展示等更多场景。

3、集成图片查看控件

由于是附件管理,因此有可能上传各种文件,包括Word文档、Excel文档、压缩文件,以及各种类型的图片,因此为了方便对图片的查看,这个控件集成了图片查看控件,可以非常方便直接读取图片附件的数据流作为对象展示,该图片控件支持对图片的滚动放大缩小、左右翻转、选择放大、图片移动、保存图片等功能,不需要查看,直接使用ESC退出即可。

当然对于其他不是图片的格式附件,由于不知道或者很难直接查看,因此提示用户保存到本地然后提示打开查看即可,如下所示。

4、集成到WCF开发框架中

为了最可能、最大程度的体现系统界面的一致性和应用完备性,我也开发了适用于WCF开发框架的附件上传模块,这样就可以在更多的开发场合上使用,而且由于附件管理模块的集中化,更加方便维护代码了。

其实WCF开发框架模式下的附件管理更有意义,因为如果是纯粹的本地文件管理,可能体现不出网络化的附件管理优势,这样通过WCF的架构,所有的附件数据都可以在各个不同的地方、各个不同的网络环境下进行访问,分布式的优势更加明显,这也是WCF开发框架的相同优势。

以上就是我对附件管理模块的封装,希望朝着WInform业务模块组件化、最终产品高度定制化的理想前进,以最快的速度搭建好最终产品,以高稳定性和统一性的组件界面或者客户的信赖和赞许。

进一步来说,我的模块化的Winform开发框架,对开发业务系统的企业来说,甚至只需要个别人掌握组件代码的维护和更新,让更多的开发人员投入到实际的业务开发或者控件使用的阵营中去即可,既可有效保护产品的安全性和统一性,也可以更高效率的开发一个新系统,而不需要企业什么基础性模块都需要开发人员参与,重新弄出一堆很难统一化的基础性产品来。回应开头的一句话,就是术业有专攻,更是厚积薄发的积累的体现。

在之前几篇文章中,介绍了关于Apsose.cell这个强大的Excel操作控件的使用,相关文章如下:

使用Aspose.Cell控件实现Excel高难度报表的生成(一)

使用Aspose.Cell控件实现Excel高难度报表的生成(二)

使用Aspose.Cell控件实现多个Excel文件的合并

这几篇文章,都对Apose.Cell这个控件生成各种Excel的方式进行了阐述,对直接把DataTable或者IList生成Excel的操作,对通过模板方式实现自定义报表的各种方式,以及多个文件的合并的方式进行了介绍。

本文继续介绍该控件进一步的使用,也主要介绍如何动态生成(不使用模板文件)各种单元格,以及图表的增加等功能,介绍生成的表格完全自定义,这个报表时一个典型的图文并茂的统计报表,它的最终样式如下所示。

这个报表,表格数据是动态生成,图形则是直接从窗体的图表控件(如ZedGraph图表控件)或者PictureBox控件中获取,写入Excel文档中的。

测试程序主界面如下所示。

和很多其他的Excel操作控件(NPOI、Myxls)一样吗,Apose.Cell也提供了WorkBook、WorkSheet、Range、Cell这些对象的包装,操作这些对象,基本上能够满足我们各种复杂的需求,给我们更加弹性化的操作。

1、 添加基本对象进行操作

            Workbook workbook = new Workbook();
            Worksheet worksheet = workbook.Worksheets[0];

这几个对象是操作Excel必须的,如果需要设定生成的Excel打印预览的参数,进行一些属性设置,如下所示。

            Workbook workbook = new Workbook();
            Worksheet worksheet = workbook.Worksheets[0];

            worksheet.PageSetup.Orientation = PageOrientationType.Landscape;//横向打印
            worksheet.PageSetup.Zoom = 100;//以100%的缩放模式打开
            worksheet.PageSetup.PaperSize = PaperSizeType.PaperA4;

我们知道,由于该报表完全是手工生成,报表的标题,以及下面几行说明文字,也是需要生成的,操作其实就是把一些单元格合并为一个区域(Range),然后赋值,改变样式就可以了,如下所示的效果和代码

            Range range; Cell cell;
            int colSpan = 4 + DeptNameList.Count * 2;
            range = worksheet.Cells.CreateRange(0, 0, 1, colSpan);
            range.Merge();
            range.RowHeight = 20;
            range.Style = CreateTitleStyle(workbook);
            cell = range[0, 0];
            cell.PutValue("患病情况统计");

            range = worksheet.Cells.CreateRange(1, 0, 1, colSpan);
            range.Merge();
            range.RowHeight = 15;
            cell = range[0, 0];
            cell.PutValue("所选部别范围内,总计有1000名人员,查询统计结果如下:");

            range = worksheet.Cells.CreateRange(2, 0, 1, colSpan);
            range.Merge();
            range.RowHeight = 15;
            cell = range[0, 0];
            cell.PutValue("自2007-1-1开始到现在,统计共有500人有患病史,累计900人次,患病情况如下表:");

2、生成报表头部表格

报表中最复杂的是表头的生成,因为它是不规则的表头,因此需要很细的操作Cell和Range对象,实现复杂表头(也是很常见的)的生成。

这个是慢工出细活,需要对Cell和Range熟悉使用,然后给他们分配不同的行列就可以生成一个标准如下的表头了。

            Style headStyle = CreateStyle(workbook, true);
            Style normalStyle = CreateStyle(workbook, false);
            int startRow = 4;
            range = worksheet.Cells.CreateRange(startRow, 0, 2, 1);
            range.Merge();
            range.Style = headStyle;
            cell = range[0, 0];
            cell.PutValue("序号");
            cell.Style = headStyle;

            range = worksheet.Cells.CreateRange(startRow, 1, 2, 1);
            range.Merge();
            range.Style = headStyle;
            range.ColumnWidth = 40;
            cell = range[0, 0];
            cell.PutValue("疾病名称");
            cell.Style = headStyle;

            int startCol = 2;
            foreach (string deptName in DeptNameList)
            {
                range = worksheet.Cells.CreateRange(startRow, startCol, 1, 2);
                range.Merge();
                range.Style = headStyle;
                cell = range[0, 0];
                cell.PutValue(deptName);

                cell = worksheet.Cells[startRow + 1, startCol];
                cell.PutValue("人次");
                cell.Style = headStyle;
                cell = worksheet.Cells[startRow + 1, startCol + 1];
                cell.PutValue("百分比");
                cell.Style = headStyle;

                startCol += 2;
            }

            range = worksheet.Cells.CreateRange(startRow, startCol, 1, 2);
            range.Merge();
            range.Style = headStyle;
            cell = range[0, 0];
            cell.PutValue("合计");

            cell = worksheet.Cells[startRow + 1, startCol];
            cell.PutValue("人次");
            cell.Style = headStyle;
            cell = worksheet.Cells[startRow + 1, startCol + 1];
            cell.PutValue("百分比");
            cell.Style = headStyle; 
            #endregion

3、填入表格内容

这个不算复杂,只需要遍历然后生成内容到单元格即可。

            //写入数据到Excel
            startRow = startRow + 2;            
            for (int i = 0; i < dt.Rows.Count; i++)
            {
                startCol = 0;
                for (int j = 0; j < dt.Columns.Count; j++)
                {
                    DataRow dr = dt.Rows[i];
                    cell = worksheet.Cells[startRow, startCol];
                    cell.PutValue(dr[j]);
                    cell.Style = normalStyle;

                    startCol++;
                }
                startRow++;
            }

4、插入图表及导出打开操作

这个Apose.Cell控件的WorkSheet提供了worksheet.Pictures.Add方法,可以添加图片的操作,不过图片是通过流方式写入,我们把图表的Image对象转换一下,创建一个内存流就可以了。如下所示。

            //写入图注
            startRow += 1;//跳过1行
            range = worksheet.Cells.CreateRange(startRow++, 0, 1, colSpan);
range.Merge();
range.RowHeight
= 15;
cell
= range[0, 0];
cell.PutValue(
"以柱状图展示如下:");//插入图片到Excel里面 byte[] bytes = ImageHelper.ImageToBytes(this.pictureBox1.Image);using (MemoryStream stream = newMemoryStream(bytes))
{
worksheet.Pictures.Add(startRow,
0, stream);
}
//Save the excel file. string saveFile = FileDialogHelper.SaveExcel("rangecells.xls", "C:\\");if (!string.IsNullOrEmpty(saveFile))
{
workbook.Save(saveFile);
if (MessageUtil.ShowYesNoAndTips("保存成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(saveFile);
}
}

至此,基于Apose.Cell的自定义报表的另外一种操作也全部实现了,为了实现这个简单的例子,以便在项目中使用,花了不少时间,不过以后对于生成这类复杂的和自定义报表,可以直接利用它们基础的对象进行操作即可;

如果是一些常规的报表,可以利用自定义模板的方式生成,然后绑定数据源;如果是二维表,或者ILIst集合,导出Excel就更简单了。以上两种都可以直接利用封装好的AsposeExcelTools来进行操作,这个通用的类库,可以省却每次去编写代码的繁琐,提高效率。