求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
 MVP+WCF+三层结构搭建项目框架
 
发布于2013-7-10
 

最近,我一直在重构之前做的一个项目,在这个过程中感慨万千。原先的项目是一个运用了WCF的C/S系统,在客户端运用了MVC模式,但MVC的View、Model耦合以及WCF端分布式欠佳等问题让我有了重构的想法,经过了一段时间的改造,逐渐形成了MVP+三层结构+WCF的面向服务的程序架构。在这里我把我的想法写成了一个例子,供大家参考。

在正式开始讲解之前,我必须得感谢Artech、代震军等诸多大虾,他们的文章给了我很大的启发。

我写的这个例子是关于博客管理的,逻辑很简单,就是用户发表文章、发表评论,管理员可以对用户进行管理。让我们先从MVP的运用开始讲起。

MVP与MVC的选择

关于MVP和MVC,我只谈谈在重构过程中的看法。在经典的MVC中,View会通过Controller调用Model中的方法,Model被更新后会立即通知View,因此View与Model还是存在一定程度的耦合的,Controller也只是作为一个简单的消息分发器,View与Controller也紧紧的贴合好像是一个整体。而在MVP中,Presenter代替Controller而出现,成为View与Model解耦的关键,在MVP中,Presenter掌管大权,View的请求提交给Presenter,由Presenter调用Model,Model更新后的状态再返回给Presenter,由Presenter控制View进行相应的显示。如此,Model与View被完全解耦,View与Controller也形成了单向依赖。MVC与MVP可以说各有特点,但我更倾向于MVP。

关于MVC和MVP,给大家看这两张图就很明白了。对于刚接触MVP的朋友可能不知道View、Presenter和Model中具体该实现哪些东西,我会在用实例讲解的时候告诉大家我的想法。

MVP实战运用

在确定使用MVP模式之后,我给我的博客程序安排了如下几个项目:

1、Main:主程序入口点。

2、Common:存放委托和公共组件等。

3、Model:MVP中的M,注意区别于三层中的Model。

4、Presenter:MVP中的P。

5、View:MVP中的V。

6、DTO:这个项目其实和三层中的Model作用是一样的,但是我为了区别于MVP中的Model,把它叫做DTO,其实它的作用就是一个DTO。

项目之间的引用关系是这样的:

Main直接调用Presenter启动程序。Presenter与View,Presenter和Model分别都是单向引用,而View和Model完全没有任何联系。View和Presenter需要引用Common,使用其中的一些公共组件,而DTO作为存放数据传输对象的项目View、Presenter和Model必须都要引用。

在搭建好项目框架之后,我们可以先从Model入手,因为Model是业务逻辑的所在地,是数据的提供者,它完全基于用例的。按照本例需求,分析出三个实体,分别是User(用户),Note(文章),Comment(评论),我们就以User为例,写一个Model。代码如下所示:

View Code 
/// <summary>
/// The interface of user model
/// </summary>
public interface IUserGroup
{
#region --Methods--
/// <summary>
/// Get all users
/// </summary>
/// <returns>Users</returns>
IList<User> GetAllUsers();

/// <summary>
/// Get user by user id
/// </summary>
/// <param name="id">the user id</param>
/// <returns>User</returns>
User GetUserById(string id);

/// <summary>
/// Update user
/// </summary>
/// <param name="user">the user</param>
void UpdateUser(User user);

/// <summary>
/// Delete user by user id
/// </summary>
/// <param name="userId">the user id</param>
void DeleteUser(string userId);
#endregion
}

这是一个用于处理User业务逻辑的接口,其中使用到的User类就是DTO中的User实体。Model中把接口公开出来供Presenter调用就可以了,Note和Comment的代码也类似,P-M之间的交互还是比较简单的。

OK,设计完Model,我们再来看看界面如何呈现,我的想法如图所示:

界面很丑陋,大家就凑合看吧。窗口就一个,有一个DataGridView用来显示所有用户,当选中一行时在下面的TextBox中显示用户的详细信息。我们所能看到的这个UI界面就是MVP中View了。设计View时需要注意的是,View一定要针对接口设计,不要针对实现,因为我们的设想是将View做成Passive View,一定要让Presenter依赖于一个抽象的View。

在本例中,我将这个界面分解为两个View,一个是显示用户详细信息的主窗体(IUserControlView),一个是显示所有用户信息的列表(IGridView),可以看到IGridView是IUserControlView的一部分。代码如下所示:

IUserControlView
/// <summary>
/// The interface of UserControlView
/// </summary>
public interface IUserControlView
{
#region --Event--
/// <summary>
/// Occurs when the view was loaded
/// </summary>
event EventHandler OnViewLoad;
#endregion

/// <summary>
/// set the UserGridView
/// </summary>
IGridView UserGridView
{
set;
}

#region --Methods--
/// <summary>
/// Initialize the components of this view
/// </summary>
void Initialize();

/// <summary>
/// Show one user information at the interface
/// </summary>
/// <param name="user"></param>
void ShowUserInfo(User user);

/// <summary>
/// Show alert form
/// </summary>
/// <param name="message">Messages should be shown</param>
void Alert(string message);
#endregion
}

IUserControlView
/// <summary>
/// The interface of UserControlView
/// </summary>
public interface IUserControlView
{
#region --Event--
/// <summary>
/// Occurs when the view was loaded
/// </summary>
event EventHandler OnViewLoad;
#endregion

/// <summary>
/// set the UserGridView
/// </summary>
IGridView UserGridView
{
set;
}

#region --Methods--
/// <summary>
/// Initialize the components of this view
/// </summary>
void Initialize();

/// <summary>
/// Show one user information at the interface
/// </summary>
/// <param name="user"></param>
void ShowUserInfo(User user);

/// <summary>
/// Show alert form
/// </summary>
/// <param name="message">Messages should be shown</param>
void Alert(string message);
#endregion
}

实现代码如下:

public   class UserGridView : DataGridView, IGridView
       public class UserDetailForm : Form, IUserControlView

View实现代码的细节我就不贴了,在文章最后会提供示例的下载链接。

需要注意的是,View既然是针对抽象设计,接口中就不能暴露任何UI实现的细节。

现在,再来看看Presenter如何设计,一般来说一个View就有一个相对应的Presenter来控制,Presenter代码如下所示:

IGridPresenter
/// <summary>
/// The interface of GridPresenter
/// </summary>
public interface IGridPresenter
{
#region --Event--
/// <summary>
/// Occurs when a user was selected
/// </summary>
event UserEventHandler OnUserSelected;
/// <summary>
/// Occurs when a user is begin to be edited
/// </summary>
event UserEventHandler OnUserBeginEdit;
#endregion

#region --Properties--
/// <summary>
/// Get the view
/// </summary>
IGridView View
{
get;
}
#endregion

#region --Methods--
/// <summary>
/// Show a group users data
/// </summary>
/// <param name="users"></param>
void ShowUsers(IList<User> users);
#endregion
}

IUserControlPresenter
/// <summary>
/// The interface of UserControlPresenter
/// </summary>
public interface IUserControlPresenter
{
/// <summary>
/// Show the mian view.Start the application
/// </summary>
void Run();
}

Presenter对View的引用是单向的,View不知道哪个Presenter在用它,View也无法访问到Presenter。我们让Presenter订阅View中的事件以响应View的请求。View是被动的,需要由Presenter控制,因此在Presenter实例化的时候同时实例化相应的View。

IGridPresenter的实现代码如下所示:

IGridPresenter Realize
public class UserGridPresenter : IGridPresenter
{
#region --Event--
/// <summary>
/// Occurs when a user was selected
/// </summary>
public event UserEventHandler OnUserSelected;
/// <summary>
/// Occurs when a user is begin to be edited
/// </summary>
public event UserEventHandler OnUserBeginEdit;
#endregion

#region --Fields--
private IGridView mView;
#endregion

#region --Properties--
/// <summary>
/// Get the GridView
/// </summary>
public IGridView View
{
get { return mView; }
}
#endregion

#region --Constructor--
/// <summary>
/// Default constructor
/// </summary>
public UserGridPresenter()
{
mView = new UserGridView();
AttachToUserGridView(mView);
}
#endregion

#region --Public Methods--
/// <summary>
/// show user data
/// </summary>
/// <param name="users"></param>
public void ShowUsers(IList<User> users)
{
mView.BindData(users);
}
#endregion

#region --Private Methods--
/// <summary>
/// Attach to the UserGridView
/// </summary>
/// <param name="view"></param>
private void AttachToUserGridView(IGridView view)
{
if (view != null)
{
view.OnUserSelected += new UserEventHandler(UserGridView_OnUserSelected);
view.OnUserBeginEdit += new UserEventHandler(UserGridView_OnUserBeginEdit);
}
}
#endregion

#region --Event Methods--
/// <summary>
/// Occurs when the OnUserSelected event in UserGridView was raised
/// </summary>
private void UserGridView_OnUserSelected(object sender, UserEventArgs e)
{
RaiseOnUserSelected(e.UserValue);
}

/// <summary>
/// Occurs when the OnUserBeginEdit event in UserGridView was raised
/// </summary>
private void UserGridView_OnUserBeginEdit(object sender, UserEventArgs e)
{
RaiseOnUserBeginEdit(e.UserValue);
}
#endregion

#region --Raise Event Methods--
/// <summary>
/// Raise the OnUserSelected event
/// </summary>
/// <param name="user"></param>
private void RaiseOnUserSelected(User user)
{
UserEventHandler handler = OnUserSelected;
if (handler != null)
{
UserEventArgs e = new UserEventArgs();
e.UserValue = user;
handler(this, e);
}
}

/// <summary>
/// Raise the OnUserBeginEdit event
/// </summary>
/// <param name="user"></param>
private void RaiseOnUserBeginEdit(User user)
{
UserEventHandler handler = OnUserBeginEdit;
if (handler != null)
{
UserEventArgs e = new UserEventArgs();
e.UserValue = user;
handler(this, e);
}
}
#endregion
}

IUserControlPresenter的实现代码如下所示:

IUserControlPresenter Realize
public class UserControlPresenterBase : IUserControlPresenter
{
#region --Fields--
private IGridPresenter mGridPresenter;
private IUserControlView mUserControlView;
#endregion

#region --Constructor--
/// <summary>
/// Default constructor
/// </summary>
public UserControlPresenterBase()
{
Initialize();
}
#endregion

#region --Public Methods--
/// <summary>
/// Show the mian view.Start the application
/// </summary>
public void Run()
{
ServiceCollection.LoadServer();
//Run as main form
Application.Run(mUserControlView as UserDetailForm);
}
#endregion

#region --Private Methods--
/// <summary>
/// Initialize this presenter
/// </summary>
private void Initialize()
{
mUserControlView = new UserDetailForm();
mGridPresenter = new UserGridPresenter();
mUserControlView.UserGridView = mGridPresenter.View;
//The UI initialize method should be executed until all sub views was assigned
mUserControlView.Initialize();
AttachToUserControlView(mUserControlView);
AttachToUserGridPresenter(mGridPresenter);
}

/// <summary>
/// Attach to the UserControlView
/// </summary>
/// <param name="view"></param>
private void AttachToUserControlView(IUserControlView view)
{
if (view != null)
{
view.OnViewLoad += new EventHandler(UserControlView_OnViewLoad);
}
}

/// <summary>
/// Attach to the UserGridPresenter
/// </summary>
/// <param name="presenter"></param>
private void AttachToUserGridPresenter(IGridPresenter presenter)
{
if (presenter != null)
{
presenter.OnUserSelected += new UserEventHandler(UserGridPresenter_OnUserSelected);
presenter.OnUserBeginEdit += new UserEventHandler(UserGridPresenter_OnUserBeginEdit);
}
}

/// <summary>
/// Show the view to edit a user
/// </summary>
/// <param name="user"></param>
private void ShowEditUserView(User user)
{
IEditUserPresenter presenter = new EditUserPresenter();
presenter.OnUserEdited += new UserEventHandler(EditUserPresenter_OnUserEdited);
presenter.ShowView(user);
}

/// <summary>
/// Show all users
/// </summary>
private void ShowAllUsers()
{
IWS_Blog blogService = ServiceCollection.BlogService;
IList<User> userList = blogService.GetAllUsers();
mGridPresenter.ShowUsers(userList);
}
#endregion

#region --Event Methods--
/// <summary>
/// Occurs when the UserControlView was loaded
/// </summary>
private void UserControlView_OnViewLoad(object sender, EventArgs e)
{
ShowAllUsers();
}

/// <summary>
/// Occurs when the OnUserSelected event in UserGridPresenter was raised
/// </summary>
private void UserGridPresenter_OnUserSelected(object sender, UserEventArgs e)
{
mUserControlView.ShowUserInfo(e.UserValue);
}

/// <summary>
/// Occurs when the OnUserBeginEdit event in UserGridPresenter was raised
/// </summary>
private void UserGridPresenter_OnUserBeginEdit(object sender, UserEventArgs e)
{
ShowEditUserView(e.UserValue);
}

/// <summary>
/// Occurs when a user edit finished
/// </summary>
private void EditUserPresenter_OnUserEdited(object sender, UserEventArgs e)
{
ShowAllUsers();
}
#endregion
}

初始化加载所有用户信息的流程是这样的:当UserControlView加载时,UserControlPresenter随之响应并调用UserModel中的GetAllUsers方法。UserControlPresenter获取到数据后,再调用GridPresenter中的ShowUsers方法,将查询的数据绑定到GridView上,这时便看到了数据。在这个过程中,View提交请求是通过事件响应实现的,只要涉及到与数据相关的请求都必须要提交到Presenter中,由Presenter来决定下一步该做什么,View中则不能包含任何与数据相关的业务逻辑。从这个流程中我们也可以看到Presenter的掌控地位,他属于指手划脚的那类人,只是在告诉别人需要做什么,但自己却不会亲自动手。

大家可能注意到了,我把UserControlView中的Initialize方法公开在接口中,并没有在UserControlView构造时执行,这是因为UserControlView依赖于一个抽象的GridView,在构造时GridView还没有被注入,显然在这个时候初始化会出现未将对象引用设置到对象的实例的异常。因此我把UserControlView的初始化工作交给Presenter处理,将初始化的时机延后到所有依赖注入完成,而View不会主动执行Initialize。

IUserControlPresenter已经是最顶层的Presenter了,它只需要公开出接口供Main调用启动程序即可。

我感觉,P-V交互是MVP模式运用的重点,本人水平有限,文中疏漏或讲解不到位的地方还请大家谅解。关于MVP,大家可以看看Artech和代震军的文章,你们会学到很多,还有一篇关于MVP的14条规则的文章也强烈推荐。

这篇文章是关于MVP运用的,下面我会介绍我是如何把WCF服务端加入进来的,以及我对三层结构运用的心得。

在上篇文章中,我对如何在项目中如何运用MVP谈了自己的看法。在本文,我将会把WCF服务端加入进来,以面向服务的角度完善我的程序。

胖客户端与瘦客户端的选择

C/S模式的程序一般会有两种形式,一种是瘦客户端(Thin Client)形式,即客户端仅处理UI界面的交互,把所有和数据相关的业务逻辑都放在服务器。另一种是胖客户端(Rich Client)形式,即客户端不仅要处理UI界面的交互,而且要完成定制业务逻辑规则的工作。Thin Client形式通常会被认为是B/S模式,毕竟浏览器可以说是最瘦的客户端了。但随着云技术的发展和对分布式要求的不断提高,传统的C/S模式也在力求使客户端轻量化,这样做的好处是更有利于业务规则的复用,将业务逻辑集中在一处也更有利于维护。Thin Client和Rich Client各有各的优点,各有各的用处,在本文的例子中更适合使用Thin Client形式。

Well,让我们回头看看我上篇文章中的例子。上文中的例子是个很典型的胖客户端,而在我们运用了MVP之后,获取数据的业务逻辑已经被完全隔离,在我划分的几个项目中,Model就是用于获取数据及处理业务逻辑的地方,因此我要把它从客户端中移出,把Model整体移植到WCF服务器。同时我们也要把DTO移植到服务端,因为他是数据的载体,服务端和客户端都需要引用,而且必须统一。

将Model移植到服务端

WCF服务器的基础知识我就不赘述了,对这方面不了解的朋友可以查阅相关书籍,园子里也有不少好文章。

我在服务端建了一个BlogService项目作为启动服务的入口,如图所示:

下面建立服务契约,服务契约就是客户端唯一能够访问的接口。我们需要将原来Model中的接口公开在这里,供客户端访问。

服务契约代码如下:

服务契约
[ServiceContract]
public interface IWS_Blog
{
#region --User Management--
[OperationContract]
/// <summary>
/// get all users
/// </summary>
/// <returns>User</returns>
IList<User> GetAllUsers();

[OperationContract]
/// <summary>
/// get user by user id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
User GetUserById(string id);

[OperationContract]
/// <summary>
/// update user by user id
/// </summary>
/// <param name="id"></param>
void UpdateUser(User user);
#endregion
}

BlogService项目中的ServiceManager类是用于管理服务的,它的代码如下所示:

ServiceManager
public class ServiceManager
{
#region --Fields--
private ServiceHost mBlogHost;
#endregion

#region --Public Methods--
/// <summary>
/// 打开所有服务
/// </summary>
public void OpenAllServices()
{
InitialOneHost("");
}
#endregion

#region --Private Methods--
/// <summary>
/// 获取本机ip
/// </summary>
/// <returns>本机ip</returns>
private string GetLocalhostIp()
{
IPHostEntry ipHost = System.Net.Dns.GetHostEntry(Dns.GetHostName());
IPAddress ipAddr = ipHost.AddressList[0];
return ipAddr.ToString();
}

/// <summary>
/// 初始化一个服务实例
/// </summary>
private ServiceHost InitialOneHost(string serviceName)
{
string serverIp = GetLocalhostIp();//获取本机ip
Uri baseAddress_Blog = new Uri("http://" + serverIp + ":8000/WS_Blog/");
mBlogHost = new ServiceHost(typeof(WS_Blog), baseAddress_Blog);
mBlogHost.Open();
return mBlogHost;
}
#endregion
}

只要在启动程序时执行OpenAllService(),便可以启动服务。

在客户端使用WCF服务时我运用了Channel Factory的形式。不了解的朋友可以查阅相关资料,服务端运行结果如图所示:

这时,客户端进行相关配置之后也可以使用了,让我们看看原来客户端的Model现在是什么样子的,客户端Model项目如图所示:

只有一个服务契约和管理服务资源的类,现在客户端的Model只不过是一个客户端访问服务的接口罢了。

ServiceCollection类的代码如下所示:

ServiceCollection 
/// <summary>
/// 服务集合
/// </summary>
public static class ServiceCollection
{
private static IWS_Blog mBlogService;

/// <summary>
/// 获取BlogService
/// </summary>
public static IWS_Blog BlogService
{
get { return mBlogService; }
}

/// <summary>
/// 加载服务器
/// </summary>
public static void LoadServer()
{
string serverEndPoint = "127.0.0.1:8000";
try
{
Uri uri = new Uri("http://" + serverEndPoint + "/WS_Blog/");
EndpointAddress baseAdress = new EndpointAddress(uri);
ChannelFactory<IWS_Blog> channel = new ChannelFactory<IWS_Blog>("WSHttpBinding_IWS_Blog");
mBlogService = channel.CreateChannel(baseAdress);
}
catch (Exception ex)
{
throw;
}
}
}

客户端启动时先执行LoadServer()加载服务,然后调用服务时只需要这样做:

调用服务示例
/// <summary>
/// Show all users
/// </summary>
private void ShowAllUsers()
{
IWS_Blog blogService = ServiceCollection.BlogService;
IList<User> userList = blogService.GetAllUsers();
mGridPresenter.ShowUsers(userList);
}

客户端运行界面如图所示:

分解服务端的Model

到此,服务端与客户端的交互工作就已告一段落了。但还是美中不足,我们可以看到服务端的Model是在做数据持久化和处理业务逻辑的工作,请注意我是用了“和”,这意味这Model有了过多的权责,它是完全可以再分解的。我想大家很容易就会想到运用三层或N层结构的知识去处理这种问题。关于三层结构的知识我就不多讲了,只给大家就谈谈我运用时的一点心得。

在分解了服务端Model之后,我的项目结构如图所示:

原来的Model被我更名为Server,主要是为了避免与三层结构中的Model概念混淆,以下我就用Server代替Model来说。

BLL、DAL分别就是三层中的业务逻辑层和数据访问层,DTO就是所谓的数据传输对象,和三层中的Model作用一样的。不同的是在这里没有表示层,因为UI在客户端,并且由MVP模式控制着,在服务端,我只是用分层原理将Server分解了。

三层结构的代码我是用动软直接生成的,这可以省去不少时间,但生成的代码可能会有部分不符合要求,稍作修改即可。当然也可以通过修改代码模版解决这个问题,在这里我就是在修改代码模版后生成的。

服务端的项目引用关系如图所示:

各个项目之间均是单向引用,我对各层规则理解是这样的:

1、DAL通过DBUtility访问数据库,DAL就是编写Sql语句的地方,其中的操作必须是最原子的并且不能包含任何业务逻辑。

2、BLL调用DAL并处理一些基础业务逻辑,这个业务逻辑应该只局限于本实体内部,而且原则上不能再出现Sql语句。应尽量避免各个BLL对象之间的耦合。

3、Server调用BLL并处理更高层的业务逻辑,处理多个BLL之间的关系。

4、BlogService调用Server提供的接口将数据返回给客户端,它不能包含任务业务逻辑。

按照我的理解,可以将调用关系描绘成这么一张图:

下面我举几个实际的例子,假设用例中有一个修改用户信息的操作,但要求用户昵称不能重复。因此,在修改前我们需要先判断昵称是否已经存在,若存在则不允许修改。那么,按照我前面所说的规则,这个业务逻辑就应该写在UserBll中,因为这个业务的范围仅仅在User对象内部,代码如下所示:

View Code 
/// <summary>
/// 更新一条数据
/// </summary>
public void Update(User model)
{
bool isExistsNickname = dal.ExistsNickname(model.Nickname);
if (isExistsNickname == false)
{
dal.Update(model);
}
else
{
throw new FaultException("对不起,您的昵称太抢手了,再换一个试试");
}
}

再举一个例子,假设有一个删除用户的操作,要求删除用户时将用户的文章和评论一并删除。因此,这个删除用户操作其实包含了删除评论、删除文章和删除用户三个操作。那么,这个业务逻辑就不应该出现在UserBll中,因为它同时涉及到了评论、文章和用户三张表,我们需要把这个逻辑放在更上层的UserGroup中,代码如下所示:

Delete User
/// <summary>
/// Delete user by user id
/// </summary>
/// <param name="userId"></param>
public void DeleteUser(string userId)
{
try
{
if (userId == null || userId == "")
{
return;
}
IList<User> users = mBlogUserBll.GetModelByUserId(userId);
if (users.Count > 0)
{
User user = users[0];
if (user.IsForzen == "0")
{
throw new FaultException("用户处于活动状态,禁止删除");
}
else
{
mBlogCommentBll.DeleteByAuthorId(userId);
mBlogNoteBll.DeleteByAuthorId(userId);
mBlogUserBll.Delete(userId);
}
}
}
catch
{
throw;
}
}
#endregion
}

至此,我对三层结构运用的理解就讲完了,希望对大家有所帮助。

由于本文主要是阐述搭建架构上的思想,因此文中的例子有很多地方做的并不是很好,例如在服务端的异常处理就很业余,大家可以参考Artech写的将EHAB与WCF结合的异常处理方式,感觉很不错。

关于MVP、WCF和三层结构的运用,也许每个人都有不同的理解,我在文章中提到的论断是我自己的看法,并不一定就是完全正确的,有许多还需要推敲。写本文的目的旨在于给大家一个参考,大家有什么意见或者建议都可以拿出来讨论,一个人的思想毕竟是有限的,我们需要集思广益。

 
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
 
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
 
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
 
分享到
 
 


使用decj简化Web前端开发
Web开发框架形成之旅
更有效率的使用Visual Studio
MVP+WCF+三层结构搭建框架
ASP.NET运行机制浅析【图解】
编写更好的C#代码
10个Visual Studio开发调试技巧
更多...   


.NET框架与分布式应用架构设计
.NET & WPF & WCF应用开发
UML&.Net架构设计
COM组件开发
.Net应用开发
InstallShield


日照港 .NET Framework & WCF应用开发
神华信息 .NET单元测试
北京 .Net应用软件系统架构
台达电子 .NET程序设计与开发
赛门铁克 C#与.NET架构设计
广东核电 .Net应用系统架构
更多...