2023年2月

本随笔系列主要介绍从一个Windows平台从事C#开发到Mac平台苹果开发的一系列感想和体验历程,本系列文章是在起步阶段逐步积累的,希望带给大家更好,更真实的转换历程体验。前面几篇随笔主要介绍C#和Objective-C之间的差异,本片开始介绍利用XCode来进行IOS的程序开发,对比和C#利用VS进行开发的差异,以便我们更快的融入到IOS开发的阵营。

在介绍利用XCode来开发项目前,我自己消化了几本IOS开发的书籍,以及苹果开发网站的一些自认为重要文章介绍,学习了一些视频的内容,包括网易里面的斯坦福大学的一些视频,并自己模拟了一些简单的项目代码开发,虽然不能说是已经掌握了相关的IOS开发(其实还差很远),但是也对一些基础的项目应用有了一个大概的了解,结合以前的Winfrom应用开发(很类似),也算有了一些小小的领悟吧。但介绍IOS开发,也是结合自己以前对C#开发的一些经验和理解来介绍,希望不详细,不确切的地方,大家多多指正。

1、根据项目模板创建项目

其实对开发而言,无论是对于使用C#开发的VS开发工具,还是基于Objective-C开发的XCode工具,都有基于模板生成对应的项目,这样我们可以快速生成我们需要的项目框架,这里为了介绍方便,建立一个单视图的应用程序,有点类似于我们创建一个Winform的标准应用程序,里面默认也是带有一个Form窗体的,这个XCode的项目模板也是类似。

创建一个项目,我们需要指定项目的名称、类前缀和一些相关的信息,这个和我们创建C#项目指定命名空间以及一些相关参数也有点类似,但不等同,C#里面,对于多个项目应用,我们可以通过命名空间进行区分,但是在IOS里面开发,我们就只能通过类名进行限定了,因此,必要的类名前缀是需要的,这个的类前缀,主要是用来创建项目的视图控制器或者代理类的前缀标识。

创建的项目后的视图如下所示,可以看出红色方框就是前面一个地方的设置。

2、IOS项目和Winform项目开发的差别

创建的项目,默认有一个视图控制器和一个视图代理类,还有一个空白的界面,如下所示。这个和我们在VS里面开发Winform项目很类似,也是需要拖动控件进行界面设计的。

IOS里面也提供了很多相关的控件,作为界面设计的需要,当然除了拖动方式,在Objective C里面也可以通过代码方式创建相关的控件对象,这个和C#创建控件一样,没什么差别。

但是,当我们把控件拖动放到设计面板里面后,发现双击不再是处理响应控件操作的代码了,而是修改控件的标题,这个开发观念和C#里面开发Winform应用不同,这个是因为IOS的开发是严格遵循MVC的设计模式,它的界面设计独立的,界面视图的控制器又有独立的类进行处理,另外,它的一些处理,还可以放到一些代理类里面进行处理(如上图截图里面的应用程序委托类),这个其实有点类似于WPF的方式了,界面可以通过其他工具进行设计开发。

一般来说,我们在XCode中创建一个视图的时候,同时也会创建一个空白的XIB后缀名的视图界面,视图控制器就是管理模型数据(和模型逻辑调用),然后以一定的方式呈现视图内容,也就是说,控制器对模型和视图都有全权的控制处理;而视图如果要通知控制器,一般在IOS里面采用代理的方式或者Target-Action方式进行通信,它的关系可以通过斯坦福教授那个IOS开发介绍的图形进行阐释,如下所示。

我们如果是从事过C#的Winform开发的,或者WebForm的话,那么可能很不习惯,在IOS里面,界面设计和处理代码是分开的,它利用XML来描述界面(XIB文件),另外,对于界面的后台处理,通过视图控制器进行,而每个视图里面,可能还会放置一些子视图,如WebView,TableView等这样的视图,如果需要这些具体的视图操作,那么还需要在视图控制器里面使用视图代理接口,如子视图的绘制和数据源的接口,选择视图某部分等操作,都是通过代理接口进行处理,而对于一些按钮的操作,则是通过Target-Action这样的模式进行响应。反观C#里面的Winform开发,可能不会划分那么复杂,多数是通过事件等方式对数据进行绑定或者按钮事件的响应,要了解IOS的开发模式,可能这里需要特别注意调整的地方。

随着认识的深入,你会慢慢了解这个模式对于界面的设计,代码的维护分别的很清楚,维护很方便。

3、添加一些界面控件和处理

首先,我们和在VS里面开发WInform项目一样,需要先绘制好界面,我们先把界面设计好向如下图形所示,这个工作没有什么特别之处,就是把它拖动到面板上,调整位置和布局即可。

为了好看,我在XCode的项目里面导入了一个图片,然后指定了按钮的背景图片,如下所示。

运行界面,看到的效果和这个差不多,不过如果要在代码里面对界面元素(如文本输入、标签、按钮等)进行处理,目前还不可以,需要增加一个IBOutLet的对象到代码里面,一般可以通过拖动的方式添加。

拖动的方式,就是按着Control拖动界面元素到代码里面,会弹出一个输入界面,输入控件的名称即可,这个变量称之为插座(IBOutlet)变量,如下所示。

对于按钮的事件处理,也可以通过这个方式进行添加,如下所示,

最后视图实现类的代码如下所示

#import "MyHelloAppViewController.h"

@interfaceMyHelloAppViewController ()

@property (weak, nonatomic) IBOutlet UITextField
*txtName;

@property (weak, nonatomic) IBOutlet UITextField
*txtAge;- (IBAction)getResult:(id)sender;@end

然后我们在后台代码里面,实现getResult的方法内容,把输入的内容在弹出视图里面进行展示,如下所示。

- (IBAction)getResult:(id)sender
{
NSLog(
@"姓名:%@ 年龄:%@",self.txtName.text, self.txtAge.text);

UIAlertView
*message =[[UIAlertView alloc] initWithTitle:self.txtName.text
message:self.txtAge.text
delegate:self cancelButtonTitle:@"取消"otherButtonTitles:@"保存", nil];
message.alertViewStyle
=UIAlertViewStyleDefault;
[message show];
}

运行界面后,得到的效果如下所示。

4、界面的横向布局

我们知道,一般的IOS应用,可以竖着使用,也可以横着使用,如果界面支持,那么横着的话,也应该看起来很美观才可以,例如IOS里面的浏览器就是很好的例子,横竖总适宜的。

我们上面方式创建的界面,虽然界面元素比较少,但是一旦横着来看,那么它的效果就是固定宽度的了。

如果需要让它们的元素能够根据界面自动调整,就需要设置控件的约束,如下所示。

一旦添加了约束,我们选择输入框后,就会看到几个方向上的一些线条提示,如下所示。

通过指定几个不同方向的约束,就能够很好的控制输入框的大小了,让它可以根据界面的调整而自动进行调整,运行模拟器,得到的界面效果如下所示,输入框的宽度已经能够自动伸缩了。

本随笔系列主要介绍从一个Windows平台从事C#开发到Mac平台苹果开发的一系列感想和体验历程,本系列文章是在起步阶段逐步积累的,希望带给大家更好,更真实的转换历程体验。本篇主要开始介绍基于XCode进行IOS程序的开发,介绍其中视图控制器的理解和使用过程。

1、视图控制器(导航视图控制器和选项卡视图控制器)

我们知道,在.net里面开发Winform程序或者Webform程序,我们只需要绘制一个个界面,然后在后台添加相应的处理,如果设计比较好的话,把业务逻辑、数据库访问等不同的层包装起来,本篇不考虑业务规则这块,主要讨论界面视图的处理。在IOS开发里面,同样也需要通过XIB设计好相关的界面,和Winform不同,IOS采用了MVC模式来设计界面和界面后台处理操作。这个XIB只是一个界面内容,还需要一个和UI对应的视图控制器,视图控制器和XIB界面内容一起,构成了类似于Winform界面和后台代码的功能了。

这里面比较典型的视图就是导航控制器(Navigation Controller)和Tab选项卡视图控制器(Tab Bar Controller)了,这两类是属于视图容器类的视图控制器,对于内容视图,那么就是表视图(Table View)最为典型了。

我们知道,IOS的应用程序,是一个单窗口的应用,它可以有很多视图在上面,而管理各种视图,就可以通过各种视图控制器进行展示了。
如对于导航控制器,我们可以理解为它除了一个导航条外,其他部分为空的视图,这些空白的地方,可以用来放置其他子视图模块,它们通过导航条进行导航,非常方便。下面图例是这个视图控制器的说明图。

通过这个视图控制器,我们可以在导航条里面放置一些返回按钮(或者自定义的一些功能),如下所示

而Tab Bar Controller我们就很熟悉了,它有点类似在.NET的Winform开发里面的TabControl控件,它里面还需要放置一些特殊的内容才能组合成实际的界面。

选项卡视图里面和导航视图控制器一样,除了选项卡底部有一排按钮方便切换视图外,其他部分是空白的,空白的地方一般还需要放置一些视图控制器,比较典型的一个例子就是IOS里面的世界时钟的程序,它是由选项卡视图控制器,导航视图控制器以及一些其他的视图进行组合起来的,它们的分解图如下所示。

2、视图控制器的应用例子

我为了详细了解上面所说的两个视图控制器,我做了一个例子,界面如下所示,虽然简单,但是我们可以从中了解到这两个视图控制器的使用。

这个例子里面使用了上面所说的两个视图控制器,首先外部是一个选项卡视图控制器,然后第一个视图里面,用了导航视图控制器,导航视图控制器里面还可以放置几个视图,选项卡第二个视图放了一个普通的视图即可。

为了在程序启动的时候对程序的视图界面进行初始化,我们需要添加一些代码构建几个不同的视图容器,如下所示。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

self.window
=[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
LoginViewController
*loginviewController =[[LoginViewController alloc]init];
loginviewController.title
= @"用户中心";


self.navigationController
=[[UINavigationController alloc]init];
EbookStyleViewController
*bookStyleController =[[EbookStyleViewController alloc]init];

UIBarButtonItem
*backButton = [[UIBarButtonItem alloc]initWithTitle:@"返回"style:UIBarButtonItemStyleBordered target:nil action:nil];
bookStyleController.title
= @"书籍类别";

bookStyleController.navigationItem.backBarButtonItem
=backButton;

[self.navigationController pushViewController:bookStyleController animated:NO];

self.tabBarController
=[[UITabBarController alloc]init];
self.tabBarController.viewControllers
=@[self.navigationController, loginviewController];


[self.window addSubview:self.tabBarController.view];
[self.window makeKeyAndVisible];
returnYES;
}

其中LoginviewController和 UINavigationController 是同一个级别的(而EbookStyleViewController作为导航视图控制器的第一个视图),这样LoginviewController和 UINavigationController 他们就放到了TabBarController里面了。程序外观上我们好像看到是EbookStyleViewController和LoginviewController两个视图。

    self.tabBarController =[[UITabBarController alloc]init];
self.tabBarController.viewControllers
= @[self.navigationController, loginviewController];

为了更改视图导航控制器的外观显示,我们需要在EbookStyleViewController视图里面进行一些设置处理,如标题和按钮的处理。

- (void)viewDidLoad
{
UIBarButtonItem
*leftButton = [[UIBarButtonItem alloc]initWithTitle:@"查询"style:UIBarButtonItemStyleBordered target:nil action:nil];
self.navigationItem.leftBarButtonItem
=leftButton;

UITabBarItem
*item = [[UITabBarItem alloc]initWithTitle:@"书籍类别" image:[UIImage imageNamed:@"bookcase.png"] tag:0];
self.tabBarItem
=item;

[super viewDidLoad];
}

增加一个查询按钮,以及修改标题为“书籍类别”,界面运行后看到的界面如下所示。

为了通过视图控制器进入其他视图,我们可以通过下面代码进行切换即可。通过按钮的事件进行触发到第二个视图。

- (IBAction)selctStyle:(id)sender;
{
EbookNameViewController
*ebookNameController =[[EbookNameViewController alloc]init];
ebookNameController.title
= @"历史类书籍";
ebookNameController.strStyle
= @"历史";
[self.navigationController pushViewController:ebookNameController animated:YES];
}

以上就是结合两个视图控制器进行的介绍,希望大家对理解这两个视图控制器有所帮助。我们使用IOS就可以看到,为了较好的体验,很多程序都需要使用这两个视图控制器进行界面的布局排版,因此,合理运用这两个东西,应该是非常有帮助的。

本随笔系列主要介绍从一个Windows平台从事C#开发到Mac平台苹果开发的一系列感想和体验历程,本系列文章是在起步阶段逐步积累的,希望带给大家更好,更真实的转换历程体验。本篇主要开始介绍基于XCode进行IOS程序的开发,介绍使用FMDB对Sqlite数据库进行操作,以及对数据库操作类进行抽象设计,以期达到重用、简化、高效开发的目的。

在.NET领域开发了很多年,一般常见的项目都需要操作数据库,包括有Oracle、SqlServer、Mysql、Sqlite、Access等数据库,这些数据库是很常见的,我们在.NET环境里面开发的各种系统,可能都或多或少需要和其中一种以上的数据库打交道,这也是我致力于提炼我的.NET领域的Winform开发框架、Web开发框架、混合式开发框架的目的,尽可能达到简化、重用、高效开发的目的。

虽然现在在IOS领域做一些研究开发,即使IOS设备更多强调的是一个多媒体的设备,但是数据库的操作还是必不可少,因此我先从我熟悉的数据库这块入手,了解其中数据库是如何操作的,有哪些现成的组件进行参考学习等等。

在IOS里面开发,提起和数据库打交道,可能很多人都熟悉FMDB这个数据库的组件,它对IOS里面操作Sqlite数据库进行了很大程度的简化,简化后,我们大多数情况下,只需要和
FMDatabase和FMResultSet打交道即可,使用起来非常方便。

1、FMDB的操作

为了较好介绍整体性的内容,我们先从FMDB的各种操作进行介绍。

1)数据库打开操作

FMDatabase *db=[FMDatabase databaseWithPath:dbPath] ;if (![db open]) {  
NSLog(
@"无法打开数据库");return;
}

2)数据库操作executeUpdate

使用FMDB,对于没有返回记录的操作,都可以用executeUpdate进行操作,如下是删除记录的函数

- (BOOL) deleteByCondition:(NSString *) condition {
NSString
*query = [NSString stringWithFormat:@"Delete FROM %@ Where %@", self.tableName, condition];
BOOL result
=[self.database executeUpdate:query];returnresult;
}

当然,对于SQLite很多数据库操作,我们可以使用参数化语句进行操作,如下例子所示

[db executeUpdate:@"INSERT INTO User (Name,Age) VALUES (?,?)",@"张三",[NSNumber numberWithInt:30]]  

参数化也可以使用 : 字符作为参数标识,如下所示,是我封装的一个数据库操作函数

- (BOOL) deleteById:(id) key
{
NSDictionary
*argsDict = [NSDictionary dictionaryWithObjectsAndKeys:key, @"id", nil];
NSString
*query = [NSString stringWithFormat:@"Delete FROM %@ Where %@ =:id", self.tableName, self.primaryKey];
BOOL result
=[self.database executeUpdate:query withParameterDictionary:argsDict];returnresult;
}

3)返回集合的操作

操作有返回集合的语句,我们就需要用到FMResultSet对象了,这个对象类似于以前见过的游标,不过不一样的东西而已。

FMResultSet *rs=[db executeQuery:@"SELECT * FROM User"];
rs
=[db executeQuery:@"SELECT * FROM User WHERE Age = ?",@"20"];while([rs next]){
NSLog(
@"%@ %@",[rs stringForColumn:@"Name"],[rs stringForColumn:@"Age"]);
}

上面的代码操作,返回一个Resultset集合进行遍历使用,我们可以根据Resultset的一些方法获取到不同的数据内容。

相对于FMResult的操作方式,原生态的Sqlite数据库操作代码如下所示。看完是不是感觉使用FMDB类库操作数据库方便很多呢。

 NSString *sqlQuery = @"SELECT * FROM User";
sqlite3_stmt
*statement;if (sqlite3_prepare_v2(db, [sqlQuery UTF8String], -1, &statement, nil) ==SQLITE_OK) {while (sqlite3_step(statement) ==SQLITE_ROW) {char *name = (char*)sqlite3_column_text(statement, 1);
NSString
*nsNameStr =[[NSString alloc]initWithUTF8String:name];int age = sqlite3_column_int(statement, 2);char *address = (char*)sqlite3_column_text(statement, 3);
NSString
*nsAddressStr =[[NSString alloc]initWithUTF8String:address];

NSLog(
@"name:%@ age:%d address:%@",nsNameStr,age, nsAddressStr);
}
}
sqlite3_close(db);

FMResultSet方法有下面几种,分别用于获取不同的数据:

  • intForColumn:
  • longForColumn:
  • longLongIntForColumn:
  • boolForColumn:
  • doubleForColumn:
  • stringForColumn:
  • dateForColumn:
  • dataForColumn:
  • dataNoCopyForColumn:
  • UTF8StringForColumnIndex:
  • objectForColumn:

2、数据库操作层的设计

上面小节介绍了使用FMDB类库对Sqlite数据库的操作,使我们大致了解了在IOS里面对数据库操作的过程。但是单纯如果介绍这些,我觉得太泛泛了,而且对我们使用起来也还是很不方便,很多时候,我总是想通过设计的方式,来简化我的各种操作处理。如我们知道,IOS里面的Objective C也提供了很多高级语言都有的属性,如对象继承,接口、多态等等。

我很早之前写过的随笔《
Winform开发框架之数据访问层的设计
》 ,介绍了我在.NET领域里面的数据库访问层的设计,由于这种设计,能很大程度上减少代码量,并提高开发效率,因此,我也想再IOS里面形成这样的数据访问设计,虽然IOS里面可能主要是使用单机版的数据库,如SQLite数据库,所以我想简化一些.NET里面的设计模型。

在.NET里面,我的框架分层主要如下所示,这种框架的设计模式,已经很好应用在了我的Winform开发框架、WCF及混合式开发框架、Web框架里面。它们常见的分层模式,可以分为UI层、BLL层、DAL层、IDAL层、Entity层、公用类库层等等

而其中的DAL层的设计,示意图如下所示,DAL层主要是通过继承自BaseDAL基类(如BaseDALSQL)进行, BaseDALSQL进行更高一层的抽象,已达到更好的应用目的。

而我们在IOS里面,则可以主要考虑SQLite数据库即可,因此,我把设计通过简化,构造下面的设计模型

在项目里面,数据访问层的文件如下所示(为了演示测试方便,使用User表进行操作)。

为了实现数据的承载,我们需要把表的数据转换为实体类进行显示和操作,因此实体类的设计也需要考虑好。由于数据访问层的基类,封装了大多数的数据操作,也包括返回的数据对象和集合,因此数据访问层的基类,也涉及到了数据类型返回的问题。

由于IOS里面的Objective C里面没有泛型这样的东西,因此有两种方式可以来实现基类实体类的处理:一是使用动态类型id类型作为实体类类型,一种是使用一种半类型化的类型(实体类的基类)作为对象,我倾向于使用后者,因为毕竟比较接近真实的类型了。

//
//BaseEntity.h//MyDatabaseProject//
//Created by 伍华聪 on 14-4-2.//Copyright (c) 2014年 伍华聪. All rights reserved.//#import<Foundation/Foundation.h>@interface BaseEntity : NSObject

@end
//
//UserInfo.h//MyDatabaseProject//
//Created by 伍华聪 on 14-3-30.//Copyright (c) 2014年 伍华聪. All rights reserved.//#import<Foundation/Foundation.h>#import"BaseEntity.h"@interface UserInfo : BaseEntity

@property(nonatomic, copy) NSString
*ID;
@property(nonatomic, copy) NSString
*name;
@property(nonatomic, copy) NSString
*fullName;

@end

这种继承模式和.NET的继承关系差不多了。

对于数据访问层的设计代码,它就是在数据访问基类的定义如下,基类使用了FMDB的数据访问类进行操作的,里面很多操作的接口就是模仿我在.NET领域里面的数据访问层的设计。

//
//BaseDAL.h//MyDatabaseProject//
//Created by 伍华聪 on 14-3-30.//Copyright (c) 2014年 伍华聪. All rights reserved.//#import<Foundation/Foundation.h>#import"FMDatabase.h"#import"BaseEntity.h"@interface BaseDAL : NSObject
{
NSString
*pathToDatabase;
}
#pragma 数据库相关属性@property (nonatomic, strong) NSString*pathToDatabase;
@property (nonatomic,
readonly) FMDatabase *database; //数据库操作对象 @property (nonatomic, strong) NSString *tableName;//表名称 @property (nonatomic, strong) NSString *primaryKey;//主键 @property (nonatomic, strong) NSString *sortField;//排序,默认为主键 @property (nonatomic, assign, getter=isDescending) BOOL descending;//是否降序查询//将DataReader的属性值转化为实体类的属性值,返回实体类(子类必须重写) -(id) rsToEntity:(FMResultSet *)rs;//将实体对象的属性值转化为字典列表对应的键值(子类必须重写) -(NSDictionary *) dictByEntity:(BaseEntity *) info;#pragma 基础操作接口 //根据指定对象的ID,从数据库中删除指定对象 -(BOOL) deleteById:(id) key;//根据指定条件,从数据库中删除指定对象 - (BOOL) deleteByCondition:(NSString *) condition;//更新对象属性到数据库中 -(BOOL) update:(BaseEntity *) info byKey:(id) key;//插入指定对象到数据库中 -(BOOL) insert:(BaseEntity *) info;//插入指定对象到数据库中,并返回最后插入的ID -(NSInteger) insert2:(BaseEntity *) info;//查询数据库,检查是否存在指定ID的对象 - (BaseEntity *) findById:(id) key;//根据条件查询数据库,如果存在返回第一个对象 -(BaseEntity *) findSingle:(NSString *) condition;//根据条件查询数据库,如果存在返回第一个对象 -(BaseEntity *) findSingle:(NSString *) condition orderBy:(NSString *) orderBy;//根据条件查询数据库,并返回对象集合 - (NSArray *) find:(NSString *) condition;//根据条件查询数据库,并返回对象集合 - (NSArray *) find:(NSString *) condition orderBy:(NSString *) orderBy;//获取表的全部数据 - (NSArray *) getAll;//获取表的全部数据 - (NSArray *) getAll:(NSString *) orderBy;//获取某字段数据字典列表 -(NSArray *) getFieldList:(NSString *) fieldName;//获取表的所有记录数量 -(int) getRecordCount;//根据条件,获取表查询的记录数量 -(int) getRecordCount:(NSString *) condition;//根据条件,判断是否存在记录 -(BOOL) isExistRecord:(NSString *)condition;//查询数据库,检查是否存在指定键值的对象 -(BOOL) isExist:(NSString *)fieldName value:(id) value;//根据主键和字段名称,获取对应字段的内容 -(NSString *) getFieldValue:(NSString *)key fieldName:(NSString *)fieldName;//执行SQL查询语句,返回查询结果的所有记录的第一个字段,用逗号分隔。 -(NSString *) sqlValueList:(NSString *)query;#pragma 数据库初始化函数及关闭操作 //根据SQLite数据库地址初始化数据库 -(id) initWithPath:(NSString *)filePath;//根据SQLite数据库名称初始化数据库 -(id) initWithFileName:(NSString *)fileName;//关闭连接 -(void) close;@end

和我的.NET框架里面的数据访问层设计一样,数据访问基类已经封装了大多数的数据访问操作,因此各个表的数据访问对象,它的代码就可以很简洁了。从上面的基类接口设计可以看到,里面一些实体类返回函数或者列表返回函数,都使用了BaseEntity作为对象,我们具体在起子类使用的时候,把它返回的对象再一次转换即可。对于数据库访问基类,我们以一个返回集合的接口实现来分析。

- (NSArray *) find:(NSString *) condition orderBy:(NSString *) orderBy {
NSString
*query = [NSString stringWithFormat:@"SELECT * FROM %@", self.tableName];if (condition !=nil) {
query
= [query stringByAppendingFormat:@"where %@", condition];
}
if (orderBy !=nil) {
query
=[query stringByAppendingString:orderBy];
}
else{
query
= [query stringByAppendingFormat:@"ORDER BY %@ %@", self.sortField, self.isDescending ? @"DESC" : @"ASC"];
}

FMResultSet
*rs =[self.database executeQuery:query];
NSMutableArray
*array =[NSMutableArray arrayWithCapacity:[rs columnCount]];

BaseEntity
*info = nil; //默认初始化为空 while([rs next]) {
info
=[self rsToEntity:rs];
[array addObject:info];
}
[rs close];
returnarray;
}

上面代码使用了参数化的SQL语句进行查询,并且,对返回的数据库的ResultSet进行转换为实体类。

info = [self rsToEntity:rs];

由于基类封装了大多数的数据库操作函数,因此数据访问层的具体表的实现类,可以很简洁,但是已经具备了常见的CRUD操作,以及一些分页查询等复杂的数据操作功能。

//
//UserDAL.h//MyDatabaseProject//
//Created by 伍华聪 on 14-3-30.//Copyright (c) 2014年 伍华聪. All rights reserved.//#import<Foundation/Foundation.h>#import"FMDatabase.h"#import"BaseDAL.h"#import"UserInfo.h"@interface UserDAL : BaseDAL
{
}
//单例模式 +(UserDAL *) defaultDAL;

@end

基于篇幅的原因,我将在下一篇介绍如何在界面层中使用这样的数据访问设计类,先放上一些测试程序的界面截图。




在很多应用行业里面,都对数据的权限做了特别的声明,如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制,对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如默认只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据,因此不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制为佳,本文主要针对这个特点,对这个数据权限的功能模块进行探讨,并以我的Winform开发框架的实际案例进行剖析,介绍实际项目中对数据权限的控制管理。

1、权限系统的控制

刚才说到,对数据进行控制最好通过弹性的方式,在一个系统里面或者功能模块里面对用户角色或者岗位进行设置,一般权限控制默认在一个权限管理系统模块进行设定,数据权限也应该如此。

权限系统除了可以对用户能操作那些功能进行限定,也还可以对其访问那些组织机构的数据进行限定,我们通过权限系统,把这些权限控制的数据进行保存,在应用系统模块里面进行整合即可,根据角色拥有的数据权限,授予用户对其他部门或者机构的数据进行访问。如下面是我权限系统模块里面对角色权限的设置操作。

1)对角色功能权限进行设置

2)对角色数据权限进行控制

当对角色的数据权限进行保存后,我们就可以把这个角色能够访问的组织机构(公司、部门、工作组等等)进行记录起来了。

2)应用系统的集成,实现数据权限的控制

如我的一个病人资料应用系统,客户要求就是基于互联网的应用系统,因此使用WCF数据通讯模式实现数据的集中管理,而且他们要基于医院单位的数据管理模式,也就是每个单位管理各自的数据,我们可以把不同的医院单位作为不同的公司性质来区分,这样在权限模块中进行设置即可。

1)在应用程序中,通过在程序头部,让可以管理多个医院机构的用户选择管理的数据访问,即可实现不同的数据区分管理。

2)当用户在上面切换不同的机构,所有存在的界面数据全部实现刷新,如打开了很多界面,那么这些界面的数据也随之更新为对应新的机构下的数据。

了解了上面大致的需求,我们应该如何通过整合权限管理系统实现在应用系统的数据权限控制和集成呢?

首先我们需要在用户登陆的时候,获取对应用户的数据权限内容,然后把它转化为我们需要的信息,如下代码所示。

                    //判断如果用户管理的公司数据多于两个,那么就显示选择单位列表,并绑定公司数据
                    List<RoleDataInfo> roleDataList = CallerFactory<IRoleDataService>.Instance.FindByUser(info.ID);
List
<int> companyList = new List<int>();foreach (RoleDataInfo roleDataInfo inroleDataList)
{
if (!string.IsNullOrEmpty(roleDataInfo.BelongCompanys))
{
string[] companyArray = roleDataInfo.BelongCompanys.Split(',');foreach (string company incompanyArray)
{
if (!string.IsNullOrEmpty(company) &&ValidateUtil.IsNumber(company.Trim()))
{
if (!companyList.Contains(company.ToInt32()))
{
companyList.Add(company.ToInt32());
}
}
}
}
}
Portal.gc.CompanyList
=companyList;//设置选定的公司ID Cache.Instance["SelectedCompanyID"] =info.Company_ID;//设置过滤条件给界面基类使用 Cache.Instance["DataFilterCondition"] = string.Format("(Company_ID is null OR Company_ID = '{0}')", info.Company_ID);

其中CallerFactory方式调用,是以WCF的方式获取对应的数据库数据。在上面代码里面,有一个RoleDataInfo的实体类,这个就是用来承载用户角色的数据权限数据,其中包括了

BelongCompanys(所属公司)和 BelongDepts(所属部门 )的属性,我们把它解析为我需要的数据List<int> companyList 、 SelectedCompanyID 和 DataFilterCondition,当然如果有部门的控制,可以做的更多,我这里仅仅以医院机构进行区分即可。

SelectedCompanyID 就是用户选择查看的组织机构ID,DataFilterCondition就是用来构建一个数据过滤脚本,对用户看到的数据进行一个过滤筛选作用。我们把这两个数据内容,放到Winform的缓存里面,如果是Web可以用Session代替,这样可以在多个模块或者界面中方便访问使用。

为了实现用户选择不同的机构,所有打开的窗体数据实现相应的更新,那么我们需要处理公司选择的操作,具体实现代码如下所示。

        private void txtCompany_EditValueChanged(objectsender, EventArgs e)
{
//如果用户选择公司,以选择为主,否则以当前客户所在公司 if (this.txtCompany.EditValue != null)
{
CListItem item
= this.txtCompany.EditValue asCListItem;if (item != null)
{
//设置选定的公司ID Cache.Instance["SelectedCompanyID"] =item.Value;
SetSelectedCompanyName();
//设置过滤条件给界面基类使用 string condition = string.Format("Company_ID = '{0}'", item.Value);
Cache.Instance[
"DataFilterCondition"] =condition;//遍历全部窗口,更新 foreach (WHC.Framework.BaseUI.BaseDock form in this.MdiChildren)
{
form.SelectedCompanyID
=item.Value;
form.DataFilterCondition
=condition;
form.FormOnLoad();
}
string message = string.Format("您已经切换数据显示:{0}", item.Text);
MessageDxUtil.ShowTips(message);
}
}
}

上面是对所有打开的窗体,传递了对应的信息,然后进行了刷新。那么我们在看看窗体本身内部的数据显示逻辑是如何的。

我们以病人资料的查询界面为例,根据不同的输入条件,对数据进行不同查询外,还增加了一个对组织机构过滤的条件,如下所示。

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        private stringGetConditionSql()
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition =advanceCondition;if (condition == null)
{
condition
= newSearchCondition();
condition.AddCondition(
"BedNo", this.txtBedNo.Text.Trim(), SqlOperator.Like);
condition.AddCondition(
"TumorPart", this.txtTumorPart.Text.Trim(), SqlOperator.Like);........................
condition.AddDateCondition(
"InDate", this.txtInDate, this.txtInDateEnd);
condition.AddDateCondition(
"LeaveDate", this.txtLeaveDate, this.txtLeaveDateEnd);if (this.chkHasReferral.Checked)
{
condition.AddCondition(
"HasReferral", "", SqlOperator.Equal, true);
}
...................}string where = condition.BuildConditionSql().Replace("Where", "");//如果公司过滤条件不为空,那么需要进行过滤 if (!string.IsNullOrEmpty(this.DataFilterCondition))
{
where += string.Format(" AND {0}", this.DataFilterCondition);
}
return where;
}
/// <summary> ///绑定列表数据/// </summary> private voidBindData()
{
//entity this.winGridViewPager1.DisplayColumns = "HandNo,BedNo,MidVideo,Name,Sex,IdentityCard,Age,Birthday,HospitalNo,IDNumber,InDate,InDiagnosis,SurgeryDate,DirectorSurgeon,LeaveDate,TumorPart,LeaveDiagnosis,IsFirstTime,LeaveSpecimens,OuterFilm,PreMRI,PreCT,PrePicture,MidPathology,AfterCTMRI,AfterPicture,AfterVideo,LeavePicture,Endocrine,Professor,Address,Telephone,HasReferral,ReferralDate,ReferralTime,Pathology,Note,Report";this.winGridViewPager1.ColumnNameAlias = CallerFactory<IPatientService>.Instance.GetColumnNameAlias();//字段列显示名称转义 string where =GetConditionSql();
PagerInfo pagerInfo
= this.winGridViewPager1.PagerInfo;
List
<PatientInfo> list = CallerFactory<IPatientService>.Instance.FindWithPager(where, refpagerInfo); this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<PatientInfo>(list);this.winGridViewPager1.PrintTitle = "病人基本资料信息报表";
}

以上绑定代码实现了:分页、条件查询、高级查询、字段列表显示、中文名称转义,以及最重要的,根据公司条件进行数据过滤的操作,从而让用户只能管理自己的数据。

以上就是结合权限管理系统模块,在应用系统中实现功能权限控制和数据权限的控制的操作例子和代码展示,希望对大家有帮助。

最近花了不少时间在重构和进一步提炼我的Web开发框架上,力求在用户体验和界面设计方面,和Winform开发框架保持一致,而在Web上,我主要采用EasyUI的前端界面处理技术,走MVC的技术路线,在重构完善过程中,很多细节花费不少时间进行研究和提炼,一步步走过来,也积累了不少经验,本系列将主要介绍我在进一步完善我的Web框架基础上积累的经验进行分享,本随笔主要介绍利用jQuery Tags Input 插件显示选择记录。

我在利用jQuery Tags Input 插件之前,一直想找一个合适的Jquery插件或者合适的做法,来实现我Winform框架里面权限系统的一个用户选择场景,就是能够记录用户的选择,并最终能够保存到数据库里面去。在WInform里面,我可以用自定义控件的方式,很好地实现了这个功能,但是在Web界面上,我尝试用Jquery试过了很多方法,没能实现这个效果,花了不少时间来寻找,终于找到这个不错的插件。

先来看看我的最终实现的Web界面效果,就是在权限管理系统里面,机构包含的用户编辑界面,或者是角色包含人员的编辑界面里面,提供一个地方来记录用户的选择,用户确认后,可以把记录的内容保存到数据库里面。

上图下面一个区域 “
选择的用户
” 里面就是我用到这个控件来展示用户选择的人员信息。

其实这个jQuery Tags Input 插件主要的用途,是用来记录用户输入的标签的,它可以在空白的地方接受输入的内容的,如下所示。

不过这个JQuery的插件,也能够屏蔽用户的输入,由我们通过Javaascript进行添加即可,因此就正好符合我上面例子的需求了,这个jquery插件的下载地址是(
http://xoxco.com/projects/code/tagsinput/
),GitHub的下载地址是(
https://github.com/xoxco/jQuery-Tags-Input
)。

1、jQuery Tags Input 插件的使用

应用脚本和样式文件,如下所示。

<scriptsrc="jquery.tagsinput.js"></script>
<linkrel="stylesheet"type="text/css"href="jquery.tagsinput.css" />

由于在MVC项目里面中集成使用,因此你需要整理好合适的路径,我的项目代码引用的路径如下所示。

@*Tag标签的控件应用*@<scriptsrc="~/Content/JQueryTools/Tags-Input/jquery.tagsinput.js"></script>
    <linkrel="stylesheet"type="text/css"href="~/Content/JQueryTools/Tags-Input/jquery.tagsinput.css" />

简单的例子就是在需要的表单里创建一个包含tags列表的input输入框,你可以在value里设置默认或目前有的tags,并用逗号隔开。

<inputname="tags"id="tags"value="foo,bar,baz" />

如我在MVC项目的视图里面,增加了一个层,用来放置用户选择的用户信息,和上面的例子不同,我的输入默认初始化为空,如下所示。

       <divid="tbEditChoise"data-options="region:'south',split:true,title:'选择的用户',iconCls:'icon-book'"style="padding:5px;height:150px"> 
           <divid="selectedUsers"title="选择的用户"data-options="iconCls:'icon-view'"style="height:100px">
                <inputname="tags"id="tags"value="" />
           </div>    
       </div>

这个插件可以屏蔽界面上的Tag标签输入,从而让脚本根据需要写入不同的标签。
可以使用
addTag() and removeTag()函数增加和删除掉标签,代码如下:

$('#tags').addTag('foo');
$(
'#tags').removeTag('bar');

还可以用imporTags()方法导进一组tag列表,需要注意的是这样会将value里设置tag替换掉。

$('#tags').importTags('foo,bar,baz');

如果传递参数为空,那么相当于清空列表了。

$('#tags').importTags('');

使用
tagExist()可以判断一个标签是否存在:

if ($('#tags').tagExist('foo')) { ... }

这个插件还可以接受自动提示的插入操作,如下所示。

$('#tags').tagsInput({
autocomplete_url:
'http://myserver.com/api/autocomplete'});

如果想要在增加或移除掉标签的时候增加额外的功能或触发其它动作,你可以通过onAddTag和onRemoveTag这两个参数里指定回调函。这两个函数都返回了一个标签值作为参数

$('#tags').tagsInput({
width:
'auto',
onAddTag:
function(tag){
console.log(
'增加了'+tag)
},
onRemoveTag:
function(tag){
console.log(
'删除了'+tag)
}
});

前面讲了,可以屏蔽界面的Tag标签输入,而通过脚本插入标签
,或者你想提供其它交互方式增加标签,可以增加一个值为false的
interactive参数,这样就禁止了增加标签,而其它的功能和呈现都跟原来一样。

$('#tags').tagsInput({
width:
'auto',
onRemoveTag:
function(tag){
console.log(
'移除标签:'+'"'+tag+'"')
},
interactive:
false});

这个插件完整的调用语法代码如下所示,根据需要使用即可。

$(selector).tagsInput({'autocomplete_url': url_to_autocomplete_api,'autocomplete': { option: value, option: value},'height':'100px','width':'300px','interactive':true,'defaultText':'add a tag','onAddTag':callback_function,'onRemoveTag':callback_function,'onChange': callback_function,'removeWithBackspace' : true,'minChars' : 0,'maxChars' : 0 //if not provided there is no limit,
   'placeholderColor' : '#666666'});

2、在项目中使用jQuery Tags Input 插件

前面介绍了这个插件的各种用法,其中我们看到,里面主要就是记录用户选择或者录入的名称的,但是我们在界面上显示用户内容,还需要记住对应内容的ID,因为我们需要保存选择用户的ID,而不是它的名称,那么我们应该如何操作呢?

前面也介绍了,在界面上使用,我们需要在视图里面添加一个层,用来放置这个标签内容,把它排版好就是了。

       <divid="tbEditChoise"data-options="region:'south',split:true,title:'选择的用户',iconCls:'icon-book'"style="padding:5px;height:150px"> 
           <divid="selectedUsers"title="选择的用户"data-options="iconCls:'icon-view'"style="height:100px">
                <inputname="tags"id="tags"value="" />
           </div>    
       </div>

然后我们在easyUI的datagrid控件里面,增加几个按钮,用来操作这个标签的,也就是记录,保存和清空几个重要的操作。

上图的部分代码如下所示。

toolbar: [{
id:
'btnAddChoise',
text:
'添加选择',
iconCls:
'icon-add',
handler:
function() {
addChoise();
//实现添加记录 },
},
'-', {
id:
'btnComplete',
text:
'完成选择',
iconCls:
'icon-ok',
handler:
function() {
completeChoise();
//完成选择并返回 }
},
'-', {
id:
'btnCleare',
text:
'清空',
iconCls:
'icon-remove',
handler:
function() {
cleareChoise();
//清空用户选择记录 }
},
'-', {
id:
'btnReload',
text:
'刷新',
iconCls:
'icon-reload',
handler:
function() {//实现刷新栏目中的数据 $("#grid").datagrid("reload");
}
}],

前面介绍了,我们需要显示名称,同时也要记录选择的项目ID(用户ID),那么我们可以用两个列表对象来记录它们,它们写入的顺序一样,获取的下标也就一样了。

我们先初始化列表和Tags标签对象,并增加一个添加用户的封装和移除用户的封装操作,代码如下所示。

        <script type="text/javascript">$(function() {
$(
'#tags').tagsInput({
width:
'auto',
height:
'100px',
onRemoveTag:
function(tag) {var i =addNameList.indexOf(tag);var id =addUserList[i];
removeUser(id, tag);
},
interactive:
false});
});
var addUserList = newArray();var addNameList = newArray();functionaddUser(id, name) {if ($.inArray(id, addUserList) == -1) {
addUserList.push(id);
addNameList.push(name);
$(
'#tags').addTag(name);
}
}
functionremoveUser(id, name) {if ($.inArray(id, addUserList) != -1) {
addUserList.pop(id);
addNameList.pop(name);
$(
'#tags').removeTag(name);
}
}
</script>

清除用户选择的Tag操作,代码也很简单了,都是我小节1介绍的内容,熟练应用就是了。

            //清空用户选择记录
            functioncleareChoise() {
$(
'#tags').importTags('');
addUserList
= newArray();
addNameList
= newArray();
}

对于最重要的保存操作,就是把存储用户ID的列表,把他们传递给对应的Ajax调用就搞定了。

            //完成选择并返回
            functioncompleteChoise() {var ouid = $('#txtID').val();if (ouid != "") {var url = '/OU/EditOuUsers?r=' +Math.random();
saveAction(url, ouid, addUserList);
}

$(
"#DivEditUser").dialog('close');
reloadRelation();
//重新刷新 }//保存机构用户操作 functionsaveAction(url, id, newList) {
$.ajax({
type:
'POST',
url: url,
async:
false,
data: { ouid: id, newList: newList.join(
',') },
success:
function(result) {
$.messager.alert(
"提示", "操作成功! ");
$(
'#DivEditUser').dialog('close');
reloadRelation();
},
error:
function(xhr, status, error) {
$.messager.alert(
"提示", "操作失败"); //xhr.responseText }
});
}

最后,我们就可以顺利看到真正的结果了。

整个界面就是开始的那个了。

和我Winform权限系统里面的对应界面对比,是不是发现很接近呢?利用EasyUI创建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)--自动生成图标样式文件和图标的选择操作