分类 其它 下的文章

在博客园很多文章里面,曾经有一些介绍Office文档预览查看操作的,有些通过转为PDF进行查看,有些通过把它转换为Flash进行查看,但是过程都是曲线救国,真正能够简洁方便的实现Office文档的预览的还是比较少,这里的Office文档包括了Word、Excel、PPT文档。本文介绍两种方式,一种方式是通过在线预览的方式,利用微软的平台进行Office文档的在线查看;一种是把Office文档生成HTML文件后进行查看。然后对比他们的优缺点,并进行总结。

1、利用微软的平台进行Office文档的在线查看

一直以来,都希望找一个合适的Office文档查看的控件或者解决方案,这样客户在使用系统的时候,可以直接查看预览一些文档,而不需要安装Office,或者下载到本地在查看。

一个偶然的机会,在网上搜到微软自己提供了一个在线服务,可以实现把Office文档转换为在线的内容进行查看,大家可以通过下面的链接地址大致看看:
http://blogs.office.com/2013/04/10/office-web-viewer-view-office-documents-in-a-browser/

使用浏览器直接查看Office文档的链接地址格式如下所示:

http://view.officeapps.live.com/op/view.aspx?src=http%3a%2f%2fvideo.ch9.ms%2fbuild%2f2011%2fslides%2fTOOL-532T_Sutter.pptx

这个链接分为了两部分,一部分是http://view.officeapps.live.com/op/view.aspx?src=,后面那个是具体的文档地址,用URLEncode进行处理的链接地址。

非常酷的效果,使用这个服务唯一的要求就是你的网站是部署在互联网上的,这样服务才可以调用你的文档地址然后进行展示。

这个是使用微软的预览Office服务,当然你可以部署自己的Office预览服务,也就是需要安装Office Web Apps服务(系统要求为Windows Server 2012)

一般情况下,Office Web Apps要与其他应用配合使用,如下图所示(看起来还是很复杂的,这些东西好像也要独立安装在不同的机器):

clip_image035

好在微软给我们提供了在线的Office文档预览服务,其实好像Google Doc Viewer也是可以的,但是已经不可使用了。这样,我们就可以利用公开的接口地址实现Office文档的在线预览了。

以常用的Excel文档为例,它可以提供了完美的在线预览效果。

我们还注意到上面有一排菜单,可以展开进行相关功能的操作,尤其是文档的下载和打印很吸引。

这样我们就可以在我们的文档预览操作页面上进行集成了,下面是我在我的《基于MVC4+EasyUI的Web开发框架》集成的一个例子,对Office文档进行查看

感觉效果还不错吧。

Word文档也是可以顺利进行预览,但是就没有打印和下载的功能了,不过预览的效果还是很不错的。

2、把Office文档生成HTML文件后进行查看

上面说了,使用在线的Office预览服务来查看Office文档,如果我们的管理系统是在局域网内跑的应用,那么我们就是用不了了,那么我们如果需要使用这种在线预览Office文档的服务,应该如何操作呢?

虽然自行搭建Office Web Apps服务应用可以解决这个问题,但是一般来说,搭建这样的平台环境,太过繁琐和昂贵了,有没有更好的方式实现Office文档的查看呢?

有的,下面我来介绍一下,如何使用Aspose组件把Office文档生成HTML,然后进行查看的做法。

对应不同的Office文档,Aspose提供了不同的组件,如Aspose.Word、Aspose.Cells、Aspose.Slides等不同的组件用来处理Word/Excel/PPT等几种文档。

我们知道,Aspose组件在处理Office文档方面非常的强大,但是也是收费软件,所以需要可以购买支持,但是我们这里纯粹讨论它的功能效果。

对不同的文件类型,我们调用不同的组件进行HTML的转换就可以了,核心部分代码如下所示。

                            if (ext == "doc" || ext == "docx")
{
Aspose.Words.Document doc
= newAspose.Words.Document(templateFile);
doc.Save(generateFilePath, Aspose.Words.SaveFormat.Html);
}
else if (ext == "xls" || ext == "xlsx")
{
Workbook workbook
= newWorkbook(templateFile);
workbook.Save(generateFilePath, SaveFormat.Html);
}
else if (ext == "ppt" || ext == "pptx")
{
templateFile
= templateFile.Replace("/", "\\");
PresentationEx pres
= newPresentationEx(templateFile);
pres.Save(generateFilePath, Aspose.Slides.Export.SaveFormat.Html);
}

这样,我们第一次使用的时候,判断目录里面是否有对应的HTML文件了,如果没有,就使用上面的代码生成就可以了,查看的时候,就返回对应的路径给客户端进行查看文件就可以了。

下面是几个文档的效果截图,供参考。

3、集成在线和本地生成文件两者的预览方式

我们在做项目的时候,往往不知道具体的应用是部署在公网(互联网)的环境,还是在局域网的环境,因此要求我们很多功能需要有一定的弹性,如果能够使用公网网的,利用公网微软的Office预览功能,呈现出来的效果比使用aspose组件生成的效果更好,有时候使用aspose组件生成的文档,格式可能有一些不太一样(虽然总体还好)。

因此,如果能够根据需要,对两者能够配置一下,进行切换,应该是比较理想的方案。

下面是我根据需要对它们两者的预览方案进行了集成,通过一个变量进行切换,当然,变量可以写到配置文件里面,这样以后也可以不同修改代码就可以自由切换了。

                bool officeInternetView = false;//是否使用互联网在线预览
                string hostName = HttpUtility.UrlPathEncode("http://www.iqidi.com/");//可以配置一下,如果有必要

                if (ext == "xls" || ext == "xlsx" || ext == "doc" || ext == "docx" || ext == "ppt" || ext == "pptx")
{
if(officeInternetView)
{
//返回一个微软在线浏览Office的地址,需要加上互联网域名或者公网IP地址 viewUrl = string.Format("http://view.officeapps.live.com/op/view.aspx?src={0}{1}", hostName, filePath);
}
else{#region 动态第一次生成文件 //检查本地Office文件是否存在,如不存在,先生成文件,然后返回路径供查看 string webPath = string.Format("/GenerateFiles/Office/{0}.htm", info.ID);string generateFilePath =Server.MapPath(webPath);if (!FileUtil.FileIsExist(generateFilePath))
{
string templateFile = BLLFactory<FileUpload>.Instance.GetFilePath(info);
templateFile
= Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, templateFile.Replace("\\", "/"));if (ext == "doc" || ext == "docx")
{
Aspose.Words.Document doc
= newAspose.Words.Document(templateFile);
doc.Save(generateFilePath, Aspose.Words.SaveFormat.Html);
}
else if (ext == "xls" || ext == "xlsx")
{
Workbook workbook
= newWorkbook(templateFile);
workbook.Save(generateFilePath, SaveFormat.Html);
}
else if (ext == "ppt" || ext == "pptx")
{
templateFile
= templateFile.Replace("/", "\\");
PresentationEx pres
= newPresentationEx(templateFile);
pres.Save(generateFilePath, Aspose.Slides.Export.SaveFormat.Html);
}
}
#endregionviewUrl=webPath;
}
}
else{
viewUrl
=filePath;
}

最后,我在附件管理模块里面实现所有文档的显示,对Office文档和图片文档都可以进行预览,以及其他文档进行下载的操作。

后面进一步介绍一下Web框架上的附件管理模块的操作功能。

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

在前面介绍过Socket编程的文章中,有一篇是《
Socket开发探秘--基类及公共类的定义
》,其中介绍了一个独立线程处理类,专门在一个独立的线程中处理Socket的数据包的。摘录前面的内容介绍一下:


5
、ThreadHandler,数据独立线程处理类

对每个不同类型的数据(不同的协议类型),可以用独立的线程进行处理,这里封装了一个基类,用于进行数据独立线程的处理


上面的工作原理是这样的,每次收到数据后,系统把数据扔给独立线程处理类,处理类放到一个队列Queue的列表中,每次从中弹出一个来处理,根据不同的协议头,分派到不同的线程来处理,这样可以提高响应速度,防止线程之间的阻塞,能够充分利用系统的资源。

其实我们还可以把这个思想应用到日常的Winform开发中,有时候我们可能在处理一些比较费时的操作,可能是需要做一部分显示一部分,类似日常生活中的项目周报、月周报的场景,因为不可能等一个几年的项目完成后,你才告诉老板你的工作情况吧。

借鉴Socket的数据处理方式,我在Winform程序中运用了这种数据处理方式,如我在采集赶集网的数据的时候,可以把采集到的部分数据扔给系统中的数据独立处理线程,让他们爱怎么显示就怎么显示,程序不中断,继续乐此不彼的去采集内容去,然后继续这样做(每采集一部分仍出去一部分),直到采集完毕。




代码


public

class
ThreadHandler
<
T
>

{

///

<summary>


///
处理数据线程

///

</summary>


Thread _Handlehread
=

null
;

private

string
_ThreadName
=

""
;

private
Fifo
<
T
>
_DataFifo
=

new
Fifo
<
T
>
();


///

<summary>


///
线程名字

///

</summary>



public

string
ThreadName
{

get
{
return
_ThreadName; }

set
{ _ThreadName
=
value; }
}


///

<summary>


///
接收处理数据

///

</summary>


///

<param name="data"></param>



public

virtual

void
AppendData(T data)
{

if
(data
!=

null
)
_DataFifo.Append(data);
}


///

<summary>


///
数据处理

///

</summary>



protected

virtual

void
DataThreadHandle()
{

try

{

while
(
true
)
{
T data

=
_DataFifo.Pop();
DataHandle(data);
}
}

catch
(Exception ex)
{
LogHelper.Error(ex);
}
}


///

<summary>


///
数据处理

///

</summary>


///

<param name="data"></param>



public

virtual

void
DataHandle(T data)
{
}



///

<summary>


///
开始数据处理线程

///

</summary>



public

virtual

void
StartHandleThread()
{

if
(_Handlehread
==

null
)
{
_Handlehread

=

new
Thread(
new
ThreadStart(DataThreadHandle));
_Handlehread.IsBackground

=

true
;
_Handlehread.Start();
}
LogHelper.Info(

string
.Format(
"
[ThreadHandler] 线程->{0}启动。。。。。。
"
, _ThreadName));
}

上面的是独立线程处理的基类,下面我们用一个子类继承他,方便代码逻辑的剥离封装:

在下面的代码中,我根据不同的Table表内容类型,放到不同的函数中进行处理,以便实现不同的显示方式。


代码



public

class
TestDataHandleThread : ThreadHandler
<
PreData
>

{

public
TestDataHandleThread()
{

base
.ThreadName
=

"
测试数据操作处理线程
"
;
}


public

override

void
DataHandle(PreData data)
{

try

{

if
(data.Key
==
KeyType.PostAticle)
{

if
(
!
string
.IsNullOrEmpty(data.Content.TableName))
{
ThreadPool.QueueUserWorkItem(

new
WaitCallback(Portal.gc.MainDialog.DisplayForm), data.Content);
}
}

else

if
(data.Key
==
KeyType.ContactInfo)
{

if
(
!
string
.IsNullOrEmpty(data.Content.TableName))
{
ThreadPool.QueueUserWorkItem(

new
WaitCallback(Portal.gc.MainDialog.DisplayContactForm), data.Content);
}
}
}

catch
(Exception ex)
{
LogHelper.Error(

"
[TestDataHandleThread] 测试数据操作处理线程异常:{0}
"

+
ex.ToString());
}
}
}

下面代码是表的不同类型的枚举类和预处理数据格式定义。




代码


public

enum
KeyType{PostAticle, ContactInfo};


///

<summary>


///
预处理的数据

///

</summary>



public

class
PreData
{

private
KeyType key;

private
DataTable content;


public
KeyType Key
{

get
{
return
key; }

set
{ key
=
value; }
}


public
DataTable Content
{

get
{
return
content; }

set
{ content
=
value; }
}


public
PreData(KeyType key, DataTable data)
{

this
.key
=
key;

this
.content
=
data;
}
}

在实际的赶集网采集程序中,我需要每采集一个链接的内容后,就处理并显示,因此示例代码如下所示:




代码


///

<summary>


///
获取网站发布内容,并添加到线程进行处理

///

</summary>


///

<param name="itemDict"></param>


///

<param name="regexDict"></param>



private

void
GetContent(Dictionary
<
string
,
string
>
itemDict)
{

foreach
(
string
key
in
itemDict.Keys)
{
DataTable dt

=

new
DataTable(key);


//
标题解析,省略N行代码

//
内容解析,省略N+N行代码


//
添加到线程进行处理


Portal.gc.MainDialog.AddData(
new
PreData(KeyType.PostAticle, dt));
}
}




代码


///

<summary>


///
添加消息数据,根据不同的消息类型分派到不同的线程处理

///

</summary>


///

<param name="data">
消息数据
</param>



public

void
AddData(PreData data)
{
_testDataThread.AppendData(data);
}


///

<summary>


///
采用多线程方式显示内容数据

///

</summary>


///

<param name="data"></param>



public

void
DisplayForm(
object
table)
{
DataTable data

=
table
as
DataTable;
FrmContent content

=
FindDocument(data.TableName)
as
FrmContent;

if
(content
==

null
)
{
content

=

new
FrmContent();
content.TabText

=
data.TableName;
content.Text

=
data.TableName;
}


this
.Invoke(
new
MethodInvoker(
delegate
()
{
content.BindData(data, data.TableName);
content.Show(

this
.dockPanel);
}));
}

好了,思路是思路,程序是程序,两者结合就是实践的证明,采集大量的网站连接的时候,在也不会出现主界面停顿或者假死的情况了。下面是我闲暇时间的练笔之作, 贴图以证方案之可行。



在采集的时候,整个程序再也不会出现假死的情况,你还可以去处理其他工作的。另外,由于涉及了线程的处理工作,你还需要定时检测处理线程,如果线程有问题,还需要重启线程就可以了,这部分是属于线程检查优化的部分,不再介绍。

最近没有更新ABP框架的相关文章,一直在研究和封装相关的接口,总算告一段落,开始继续整理下开发心得。上次我在随笔《
ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用
》中介绍了字典模块的管理,以及实现了常规的获取所有记录,获取条件查询记录,创建、更新、删除这些接口。本篇继续深入介绍ABP框架在实际项目中使用的情况,本篇随笔整理对ABP基础接口,以及展示完成的省份城市行政区管理模块的内容。

1、ABP常规处理接口

根据ABP框架默认提供的一些接口,我们可以在服务端封装好相关的Web API接口(由于动态API的便利,其实是完成ApplicationService层即可),前面介绍了获取条件查询记录,创建、更新、删除这些接口的实现和处理,以及可以扩展自己的自定义业务接口,如下是字典模块的接口关系。

字典管理界面,列出字典类型,并对字典类型下的字典数据进行分页展示,分页展示利用分页控件展示。

新增或者编辑窗体界面如下

或者是批量的字典数据录入

这个精确或者模糊查询,则是在应用服务层里面定义规则的,在应用服务层接口类里面,重写CreateFilteredQuery可以设置GetAll的查询规则,重写ApplySorting则可以指定列表的排序顺序。

2、ABP常规查询接口的细化

在前面介绍了的内容汇总,基本上实现了常规数据的分页查询,我们可以看到,对于字典数据来说,分页查询条件是在DictDataPagedDto里面定义,这个是我们定义的分页条件,如下代码所示。

    /// <summary>
    ///用于根据条件分页查询/// </summary>
    public classDictDataPagedDto : PagedResultRequestDto
{
/// <summary> ///字典类型ID/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///指定值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }
}

这个类文件,我们一般把这个业务模块相关的统一放在一个文件中,例如字典数据相关的DTO放在一个DictDataDto文件里面,方便管理,如下所示。

上面是字典模块的一些基础介绍,实际上我们开发业务模块的时候,录入数据的时候,还需要一个判断的步骤,如不允许名称重复的情况。在创建新的记录和更新已有记录都需要进行必要的判断,保证数据的有效性和不重复性。

如对于省份管理界面来说,我们不能运行重复录入省份名称,那么就需要在录入数据或者更新数据的时候,进行必要的存在性判断。

那么上面的处理是如何实现的呢。

主要的界面实现代码如下所示。

if (string.IsNullOrEmpty(ID))
{
//判断存在条件 var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;if(isExist)
{
MessageDxUtil.ShowTips(
"省份名称已存在,请选择其他名称");this.txtProvince.Focus();return;
}
else{//创建新记录 tempInfo = awaitProvinceApiCaller.Instance.Create(tempInfo);
}
}
else{//判断存在条件,排除本记录同名情况 var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text, ExcludeId =ID.ToInt64() };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;if(isExist)
{
MessageDxUtil.ShowTips(
"省份名称已存在,请选择其他名称");this.txtProvince.Focus();return;
}
else{//更新记录 tempInfo = awaitProvinceApiCaller.Instance.Update(tempInfo);
}
}

ProcessDataSaved(
this.btnOK, newEventArgs());this.DialogResult = System.Windows.Forms.DialogResult.OK;

我们发现,这里增加了一个Count的函数用来判断,传入的条件就是前面的分页请求条件。

bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;

我们看看我们的应用服务层的接口实现如下所示。

        /// <summary>
        ///获取指定条件的数量/// </summary>
        /// <param name="input">查找条件</param>
        /// <returns></returns>
        public async virtual Task<int>Count(TGetAllInput input)
{
var query =CreateFilteredQuery(input);return awaitTask.FromResult(query.Count());
}

这里最终还是跳转到 CreateFilteredQuery 函数里面实现判断逻辑了。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Province>CreateFilteredQuery(ProvincePagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.ExcludeId.HasValue, t
=>t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProvinceName.IsNullOrWhiteSpace(), t =>t.ProvinceName.Contains(input.ProvinceName));
}

这里面包含了两个判断条件,一个是排除指定的ID记录,一个是匹配省份名称。

因为我们在更新记录的时候,需要判断非本记录是否有重复的名称。

//判断存在条件,排除本记录同名情况
var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text, ExcludeId =ID.ToInt64() };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;

这个ExcludeId 我们在分页条件里面增加一个固定的属性即可。

以上的分页信息,包含了实体DTO对象的一些属性,我们可以根据需要增加或者减少一部分属性。

另外我们定义的创建省份Dto对象和获取到单个实体的DTO对象,他们的定义和关系如下所示,方便我们在界面上进行操作。

    /// <summary>
    ///创建全国省份表,DTO对象/// </summary>
    public class CreateProvinceDto : EntityDto<long>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCreateProvinceDto()
{
}
#region Property Members /// <summary> ///省份名称/// </summary> [Required]public virtual string ProvinceName { get; set; }#endregion}/// <summary> ///全国省份表,DTO对象/// </summary> public classProvinceDto : CreateProvinceDto
{

}

固定这些规则后,我们也可以用代码生成工具快速生成对应的DTO文件了。

有了这些分页属性后,我们就可以在应用服务层里面定义自己的过滤规则了,如对于字典类型的应用服务层的筛选条件函数,如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<DictType>CreateFilteredQuery(DictTypePagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!string.IsNullOrEmpty(input.ExcludeId), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!string.IsNullOrEmpty(input.Name), t =>t.Name.Contains(input.Name))
.WhereIf(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!string.IsNullOrEmpty(input.Code), t => t.Code ==input.Code)
.WhereIf(
!string.IsNullOrEmpty(input.PID), t => t.PID ==input.PID);
}

上面是对于包含、相等或者不等于的三种情况的条件判断,如果我们还需要一个时间区间范围或者数值范围的判断,那么同样可以在这里进行管理规则,如下是针对产品应用服务层的过滤规则,如下代码所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Product>CreateFilteredQuery(ProductPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProductNo.IsNullOrWhiteSpace(), t => t.ProductNo.Contains(input.ProductNo)) //如需要精确匹配则用Equals .WhereIf(!input.BarCode.IsNullOrWhiteSpace(), t => t.BarCode.Contains(input.BarCode)) //如需要精确匹配则用Equals .WhereIf(!input.MaterialCode.IsNullOrWhiteSpace(), t => t.MaterialCode.Contains(input.MaterialCode)) //如需要精确匹配则用Equals .WhereIf(!input.ProductType.IsNullOrWhiteSpace(), t => t.ProductType.Contains(input.ProductType)) //如需要精确匹配则用Equals .WhereIf(!input.ProductName.IsNullOrWhiteSpace(), t => t.ProductName.Contains(input.ProductName)) //如需要精确匹配则用Equals .WhereIf(!input.Unit.IsNullOrWhiteSpace(), t => t.Unit.Contains(input.Unit)) //如需要精确匹配则用Equals .WhereIf(!input.Note.IsNullOrWhiteSpace(), t => t.Note.Contains(input.Note)) //如需要精确匹配则用Equals .WhereIf(!input.Description.IsNullOrWhiteSpace(), t => t.Description.Contains(input.Description)) //如需要精确匹配则用Equals//状态 .WhereIf(input.Status.HasValue, t => t.Status==input.Status)//成本价区间查询 .WhereIf(input.PriceStart.HasValue, s => s.Price >=input.PriceStart.Value)
.WhereIf(input.PriceEnd.HasValue, s
=> s.Price <=input.PriceEnd.Value)//销售价区间查询 .WhereIf(input.SalePriceStart.HasValue, s => s.SalePrice >=input.SalePriceStart.Value)
.WhereIf(input.SalePriceEnd.HasValue, s
=> s.SalePrice <=input.SalePriceEnd.Value)//特价区间查询 .WhereIf(input.SpecialPriceStart.HasValue, s => s.SpecialPrice >=input.SpecialPriceStart.Value)
.WhereIf(input.SpecialPriceEnd.HasValue, s
=> s.SpecialPrice <=input.SpecialPriceEnd.Value)
.WhereIf(input.IsUseSpecial.HasValue, t
=> t.IsUseSpecial == input.IsUseSpecial) //如需要精确匹配则用Equals//最低折扣区间查询 .WhereIf(input.LowestDiscountStart.HasValue, s => s.LowestDiscount >=input.LowestDiscountStart.Value)
.WhereIf(input.LowestDiscountEnd.HasValue, s
=> s.LowestDiscount <=input.LowestDiscountEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

以上就是我们深入对分页查询和判断是否存在接口的细节处理,可以包含很多自定义的条件,如等于或不等于、包含或者不包含,区间查询(大于或者小于等)条件的处理。对于省份城市行政区管理模块的重复性判断,我们通过Count函数来判断,同时在后台应用服务层对这些参数进行规则过滤即可。

为了提高客户体验和进行一些技术探索,现在正准备把我自己的客户关系管理系统CRM在做一个Web的版本,因此对基于MVC的Web界面继续进行一些研究和优化,力求在功能和界面上保持和Winform一致,本文主要介绍在我的CRM系统中用到的全国省份、城市、行政区三者的两种效果,在Winform上实现没问题,在Web上基于MVC的EasyUI实现,同样也没有问题。

1、Winform上省份、城市、行政区的联动效果

在很早的时候,我在Winform框架的一篇随笔《
Winform开发框架之字典管理模块的更新,附上最新2013年全国最新县及县以上行政区划代码sql脚本
》中介绍了在Winform版本里面的三者联动效果,界面如下所示。

并附赠送了我自己根据统计局数据修改整理后的全国省份、城市、行政区的数据脚本。全国地区的省份、城市、区县这些新政区划的最新Sql脚本下载地址如下所示:
https://files.cnblogs.com/wuhuacong/CityDistrict.rar

里面主要通过对控件本身选择的事件进行处理,然后动态获取不同的数据进行展示,大致的逻辑就是先初始化省份数据,然后省份控件选择时触发获取该省份下的城市信息,城市控件选择的时候触发获取该城市的行政区数据,大概的代码如下所示。

        private void txtProvince_SelectedIndexChanged(objectsender, EventArgs e)
{
CListItem item
= this.txtProvince.SelectedItem asCListItem;if (item != null)
{
string provinceId =item.Value;this.txtCity.Properties.BeginUpdate();this.txtCity.Properties.Items.Clear();
List
<CityInfo> cityList = BLLFactory<City>.Instance.GetCitysByProvinceID(provinceId);foreach (CityInfo info incityList)
{
this.txtCity.Properties.Items.Add(newCListItem(info.CityName, info.ID.ToString()));
}
this.txtCity.Properties.EndUpdate();
}
}
private void txtCity_SelectedIndexChanged(objectsender, EventArgs e)
{
CListItem item
= this.txtCity.SelectedItem asCListItem;if (item != null)
{
string cityId =item.Value;this.txtDistrict.Properties.BeginUpdate();this.txtDistrict.Properties.Items.Clear();
List
<DistrictInfo> districtList = BLLFactory<District>.Instance.GetDistrictByCity(cityId);foreach (DistrictInfo info indistrictList)
{
this.txtDistrict.Properties.Items.Add(newCListItem(info.DistrictName, info.ID.ToString()));
}
this.txtDistrict.Properties.EndUpdate();
}
}

2、基于MVC+EasyUI的Web上实现省份、城市、行政区的联动

有了全国的省份、城市、行政区数据,加上对三者的数据访问进行了封装,参考Winform版本的实现过程,当然在EasyUI的Web上实现起来,也是可以的。

我们先来看看实现的效果,然后在分析其中的实现思路和代码,基于MVC+EasyUI的实现效果如下所示。


上面的效果是如何实现的呢?

1)定义相关的控制器函数,提供Json数据源

为了实现控件的数据绑定,我们第一步需要为这几个控件定义一些控制器的函数,方便获取相关的数据。其中的CListItem有Text 和 Value两个属性,可以用于绑定操作。

        /// <summary>
        ///获取所有的省份/// </summary>
        /// <returns></returns>
        publicActionResult GetAllProvince()
{
List
<CListItem> treeList = new List<CListItem>();

List
<ProvinceInfo> provinceList = BLLFactory<Province>.Instance.GetAll();foreach (ProvinceInfo info inprovinceList)
{
treeList.Add(
newCListItem(info.ProvinceName, info.ProvinceName));
}
returnToJsonContent(treeList);
}
        /// <summary>
        ///根据省份名称获取对应的城市列表/// </summary>
        /// <param name="provinceName">省份名称</param>
        /// <returns></returns>
        public ActionResult GetCitysByProvinceName(stringprovinceName)
{
List
<CListItem> treeList = new List<CListItem>();

List
<CityInfo> cityList = BLLFactory<City>.Instance.GetCitysByProvinceName(provinceName);foreach (CityInfo info incityList)
{
treeList.Add(
newCListItem(info.CityName, info.CityName));
}
returnToJsonContent(treeList);
}
        /// <summary>
        ///根据城市名称获取对应的行政区划类别/// </summary>
        /// <param name="cityName">城市名称</param>
        /// <returns></returns>
        public ActionResult GetDistrictByCityName(stringcityName)
{
List
<CListItem> treeList = new List<CListItem>();string condition = string.Format("");
List
<DistrictInfo> districtList = BLLFactory<District>.Instance.GetDistrictByCityName(cityName);foreach (DistrictInfo info indistrictList)
{
treeList.Add(
newCListItem(info.DistrictName, info.DistrictName));
}
returnToJsonContent(treeList);
}

2)在视图里面添加控件绑定数据的JS代码

为了实现三个ComboBox的控件的联动效果,我们需要使用JS代码实现数据的绑定,并绑定控件的Change事件,一旦用户选择其中一个,那么可能触发其他另外一个获取数据源。

        //绑定省份、城市、行政区信息
        functionBindProvinceCity() {var province = $('#Province').combobox({
valueField:
'Value', //值字段 textField: 'Text', //显示的字段 url: '/Province/GetAllProvince',
editable:
true,
onChange:
function(newValue, oldValue) {
$.get(
'/City/GetCitysByProvinceName', { provinceName: newValue }, function(data) {
city.combobox(
"clear").combobox('loadData', data);
district.combobox(
"clear")
},
'json');
}
});
var city = $('#City').combobox({
valueField:
'Value', //值字段 textField: 'Text', //显示的字段 editable: true,
onChange:
function(newValue, oldValue) {
$.get(
'/District/GetDistrictByCityName', { cityName: newValue }, function(data) {
district.combobox(
"clear").combobox('loadData', data);
},
'json');
}
});
var district = $('#District').combobox({
valueField:
'Value', //值字段 textField: 'Text', //显示的字段 editable: true});
}

然后界面上需要摆放这几个控件。

                        <tr>      
                            <th>
                                <labelfor="Province">所在省份:</label>
                            </th>
                            <td>
                                <selectclass="easyui-combobox"id="Province"name="Province"style="width:120px;"></select> 
                            </td>
                            <th>
                                <labelfor="City">城市:</label>
                            </th>
                            <td>
                                <selectclass="easyui-combobox"id="City"name="City"style="width:120px;"></select> 
                            </td>
                        </tr>
                        <tr>
                            <th>
                                <labelfor="District">所在行政区:</label>
                            </th>
                            <td>
                                <selectclass="easyui-combobox"id="District"name="District"style="width:120px;"></select>
                            </td>
                            <th>
                                <labelfor="Hometown">籍贯:</label>
                            </th>
                            <td>
                                <selectclass="easyui-combobox"id="Hometown"name="Hometown"style="width:120px;"></select>
                            </td>
                        </tr>

OK,我们实现了数据的初始化绑定,一旦用户选择了省份数据,那么对应的城市数据列表也会更新了,选择城市,那么行政区也接着更新了,这一切似乎都搞定了?

还没有,还需要考虑对编辑状态下的数据赋值,如果实体类的信息里面,已经有数据了,那么绑定控件后,是否会正常显示呢?

3)控件内容的绑定

一般情况下,我们通过Ajax操作来获取控制器的数据,然后绑定到界面控件上,如下所示。

            $.getJSON("/Contact/FindByID?id=" + ID, function(info) {//赋值有几种方式:.datebox('setValue', info.Birthday);.combobox('setValue', info.Status);.val(info.Name);.combotree('setValue', info.PID);
                $("#ID").val(info.ID);
$(
"#Customer_ID").val(info.Customer_ID);
$(
"#HandNo").val(info.HandNo);
$(
"#Name").val(info.Name);$("#Province").combobox('setValue', info.Province);
$("#City").combobox('setValue', info.City);
$("#District").combobox('setValue', info.District);
$("#Hometown").combobox('setValue', info.Hometown);
..................
});
}

如果没有联动的效果处理,一般情况下,这种赋值的操作,没有问题的,但是我发现使用这种方法操作城市和行政区的数据显示不正常,开始百思不得其解,测试了几种方法操作,都没有使得城市、行政区的界面控件能够正常显示。

原来发现,造成这种问题的原因,可能是使用异步操作,它们的联动效果还没有处理完毕,就执行赋值操作了,导致可能数据无法正常显示。

因此改用设置为同步的操作,如下红色代码所示,把async设置为false就表示同步,测试后发现这个设置后,界面控件能够正常显示了,一切都正常,终于解决心头大患了。

            //使用同步方式,使得联动的控件正常显示
            $.ajaxSettings.async = false;//首先用户发送一个异步请求去后台实现方法
            $.getJSON("/Contact/FindByID?id=" + ID, function (info) {

以上就是我对于经常用到的全国省份、城市、行政区的Web上的联动操作的界面效果和实现代码,希望给大家提供一个参考的案例,共同提高。

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

不知阁下是否都听说过赶集网,我想对大多数人来说,应该不会太陌生,有时无聊之时,还是可以去逛逛,了解社会百态,熟悉人间风情,品味生活精彩,呵呵。

赶集网基本是按照全国城市分类的,每一个城市是相同界面,不同内容。你可以在不同城市中切换,以便关注该城市的各种信息。


对应每个城市,赶集网又有不同的分类,基本上涵盖了生活的方方面面。


进入一个特定的分类,你可以看到相关的用户文章,有些事经纪人发的,有些是普通老百姓发的,各取所需,各观所好。



好了,说到这里,请不要以为我是给赶集网做广告,呵呵,肯定不是。

我是先解剖赶集网的内容结构,为做内容采集做准备,下面先Show一下我做的赶集网采集程序先,先有一个感性的认识,也为下面的代码找一个实在的宿主,而并非纯理论的研究,哈哈。

下面分析赶集网的内容获取及程序的工作方式:

首先第一步,我们要拿到全国省市的的划分名称,这部可以去国家统计局那里找找,哈哈,我是说真的哦。

赶集网每个城市,对应一个编号,如北京对应bj,广州对应gz, 你从上面的城市划分的源码中可以找到:
<dd>
<a
href

="
http://bj.ganji.com/

"
class

="
redLink

">

北京
</a>
</dd>,这里面的内容就包含了bj的内容,后面加上ganji.com就是北京赶集网的链接地址了,花点功夫把它找出来吧。

第二个是网站内容的分类,我查过不同城市的分类好像是一样的,因此只需要获取一个城市的分类就可以了,其他的就一样。

把分类的内容保存成html,然后放到VS格式化一通,得到了内容如下所示:

下面你根据内容,编写一个正则表达式来把分类提取出来,就可以了。献上拙例,供参考。

首先我们把分类分级,一级分类是房产、二手物品、招聘等大类,二级分类表示大分类(如房产)下面的小分类,如出租房、二手房等内容类别。

下面代码是大类的获取:


代码


private

void
GetCatetory(
object
obj)

{

string
mainUrl
=

"
http://gz.ganji.com
"
;

string
DataRegex
=

"
<dt><a\\s*?href=\
"
(
?<
value
>
.
*?
)\
"
\\s*?target=\
"
_blank\
"
>(?<key>.*?)&raquo;</a></dt>
"
;

string
itemString
=

""
;

itemString
=
CSocket.GetHtmlByUrl(mainUrl);


Database db
=
DatabaseFactory.CreateDatabase();

DbCommand command
=

null
;

if
(
!
string
.IsNullOrEmpty(itemString))

{

Regex re
=

new
Regex(DataRegex, RegexOptions.IgnoreCase
|
RegexOptions.Multiline
|
RegexOptions.IgnorePatternWhitespace);

Match mc
=
re.Match(itemString);

if
(mc.Success)

{

MatchCollection mcs
=
re.Matches(itemString);

foreach
(Match me
in
mcs)

{

string
strKey
=
me.Groups[
"
key
"
].Value;

string
strValue
=
me.Groups[
"
value
"
].Value;

try


{

string
sql
=

string
.Format(
"
insert into GanjiCategory(CategoryName,CategoryUrl) values('{0}','{1}{2}')
"
,

strKey, mainUrl, strValue);

command
=
db.GetSqlStringCommand(sql);

db.ExecuteNonQuery(command);


string
tips
=

string
.Format(
"
正在处理 {0}
"
, strKey);

CallCtrlWithThreadSafety.SetText
<
Label
>
(
this
.lblSchoolTips, tips,
this
);

}

catch
(Exception ex)

{

LogHelper.Error(ex);

}

}

}

}

}

下面代码是小分类的获取:



代码


private

void
GetItemName(
object
obj)

{

string
mainUrl
=

"
http://gz.ganji.com
"
;

Database db
=
DatabaseFactory.CreateDatabase();

DbCommand command
=

null
;


string
content
=
CSocket.GetHtmlByUrl(mainUrl);

try


{

#region
获得各项列表字符串


List
<
string
>
itemHtmlList
=

new
List
<
string
>
();

string
itemRegex
=

"
<dl\\s*?class=\
"
list_.
*?
\
"
>\\s*(.*?)\\s*</dl>
"
;

Regex re
=

new
Regex(itemRegex, RegexOptions.IgnoreCase
|
RegexOptions.Singleline
|
RegexOptions.IgnorePatternWhitespace);

Match mc
=
re.Match(content);

if
(mc.Success)

{

MatchCollection mcs
=
re.Matches(content);

foreach
(Match me
in
mcs)

{

string
strValue
=
me.Groups[
1
].Value;

itemHtmlList.Add(strValue);

}

}

#endregion



#region
对每项内容进行解析


foreach
(
string
itemString
in
itemHtmlList)

{

string
cateDataRegex
=

"
<dt><a\\s*?href=\
"
(
?<
value
>
.
*?
)\
"
\\s*?target=\
"
_blank\
"
>(?<key>.*?)&raquo;</a></dt>
"
;

string
itemNameRegex
=

"
<dd>\\s*<a\\s*href=\
"
(
?<
value
>
.
*?
)\
"
\\s*target=\
"
_blank\
"
>(?<key>.*?)</a>\\((?<id>.*?)\\)</dd>
"
;


re
=

new
Regex(cateDataRegex, RegexOptions.IgnoreCase
|
RegexOptions.Singleline
|
RegexOptions.IgnorePatternWhitespace);

mc
=
re.Match(itemString);


string
categoryName
=

""
;

if
(mc.Success)

{

Match me
=
re.Matches(itemString)[
0
];

categoryName
=
me.Groups[
"
key
"
].Value;

}


re
=

new
Regex(itemNameRegex, RegexOptions.IgnoreCase
|
RegexOptions.Multiline
|
RegexOptions.IgnorePatternWhitespace);

mc
=
re.Match(itemString);

if
(mc.Success)

{

MatchCollection mcs
=
re.Matches(itemString);

foreach
(Match me
in
mcs)

{

string
strKey
=
CText.GetTxtFromHtml(me.Groups[
"
key
"
].Value);

string
strValue
=
me.Groups[
"
value
"
].Value;


try


{

//
保存内容代码




string
tips
=

string
.Format(
"
正在处理 {0}
"
, strKey);

CallCtrlWithThreadSafety.SetText
<
Label
>
(
this
.lblSchoolTips, tips,
this
);

}

catch
(Exception ex)

{

LogHelper.Error(ex);

}

}

}

}

#endregion


}

catch
(Exception ex)

{

LogHelper.Error(ex);

}

}

完成上面两步后,我们就可以继续第三部,采集赶集网的内容等信息了。