2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Net.Sockets;
6 using System.Threading;
7 using System.Net;
8
9 namespace SocketAsyncEventTest
10 {
11 class Program
12 {
13 static byte[] Buff = new byte[512];
14
15
16 static void Main(string[] args)
17 {
18 Socket sK = new Socket(AddressFamily.InterNetwork,
19 SocketType.Stream,
20 ProtocolType.Tcp);
21
22 //建立SocketAsyncEventArgsPool池子,和Buff池子来解决多客户端问题
23 SocketAsyncEventArgs asyncArgs = new SocketAsyncEventArgs();
24 asyncArgs.SetBuffer(Buff,
25 0,
26 Buff.Length);
27 asyncArgs.Completed+=new EventHandler<SocketAsyncEventArgs>(AsyncArgs_Completed);
28
29 ManualResetEvent connEvent = new ManualResetEvent(false);
30 asyncArgs.UserToken = connEvent;
31
32 asyncArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.0.42"), 9999);
33 if (!sK.ConnectAsync(asyncArgs))
34 {
35 if (sK.Connected)
36 {
37 asyncArgs.UserToken = sK;
38 Console.WriteLine("Connect Success!");
39 if (!sK.ReceiveAsync(asyncArgs))
40 {
41 ProcessReceive(asyncArgs);
42 }
43
44 ThreadPool.QueueUserWorkItem(new WaitCallback(SendCallback), asyncArgs);
45 }
46 else {
47 Console.WriteLine("Connect Faild!");
48 }
49 }
50 else
51 {
52 if (connEvent.WaitOne(10 * 1000))
53 {
54 asyncArgs.UserToken = sK;
55 Console.WriteLine("Connect Success!");
56
57 if (!sK.ReceiveAsync(asyncArgs))
58 {
59 ProcessReceive(asyncArgs);
60 }
61
62 ThreadPool.QueueUserWorkItem(new WaitCallback(SendCallback), sK);
63 }
64 else
65 {
66 Console.WriteLine("Connect Timeout!");
67 }
68 }
69
70 connEvent.Close();
71 Console.ReadKey(false);
72 }
73
74 static void AsyncArgs_Completed(object sender, SocketAsyncEventArgs e)
75 {
76 if (e.SocketError == SocketError.Success)
77 {
78 switch (e.LastOperation)
79 {
80 case SocketAsyncOperation.Connect:
81 ManualResetEvent connEvent = e.UserToken as ManualResetEvent;
82 if (connEvent != null)
83 {
84 connEvent.Set();
85 }
86 break;
87 case SocketAsyncOperation.Receive:
88 ProcessReceive(e);
89 break;
90 }
91 }
92 else
93 {
94 //SafeShutdown, 并把SocketAsyncEventArgs 放回池子里
95 }
96 }
97
98 static void ProcessReceive(SocketAsyncEventArgs args)
99 {
100 /*
101 * 如何避免被放回池子的对象还在原来的Socket中继续Receive呢,
102 * 这里就需要判断一下状态咯
103 */
104 if (args.SocketError == SocketError.Success)
105 {
106 if (args.BytesTransferred > 0)
107 {
108 byte[] buff = new byte[args.BytesTransferred];
109 Buffer.BlockCopy(args.Buffer,
110 args.Offset,
111 buff,
112 0,
113 buff.Length);
114 Console.WriteLine("Receive:{0}", Encoding.UTF8.GetString(buff));
115 }
116 Socket sK = args.UserToken as Socket;
117 if (sK != null)
118 {
119 if (!sK.ReceiveAsync(args))
120 {
121 ProcessReceive(args);
122 }
123 }
124 }
125 }
126
127 static void ProcessSend(IAsyncResult iar)
128 {
129 Socket sK = (Socket)iar.AsyncState;
130 if (sK.Connected)
131 {
132 try
133 {
134 sK.EndSend(iar);
135 }
136 catch (Exception ex)
137 {
138 Console.WriteLine(ex.Message);
139
140 //SafeShutdown,对于SocketAsyncEventArgs的回收,需要IAsyncResult来传递不同的UserToken
141 }
142 }
143 }
144
145 static void SendCallback(object args)
146 {
147 Socket sK = (Socket)args;
148 byte[] buff = Encoding.UTF8.GetBytes("Hello SocketAsyncEventArgs!");
149 while (true)
150 {
151 if (sK.Connected)
152 {
153 sK.BeginSend(buff,
154 0,
155 buff.Length,
156 SocketFlags.None,
157 new AsyncCallback(ProcessSend),
158 sK);
159 }
160 else
161 {
162 break;
163 }
164 Thread.Sleep(10);
165 }
166 Console.WriteLine("Stop Send!");
167 }
168 }
169 }
C/S分布式开发相比BS开发要考虑更多问题,难度也相对要高。
本文以最基本的Client端请求展示数据为例来讨论一下C/S分布式开发中的用户体验!
在本文中你将看到关于C/S分布式设计中可能需要考虑的问题,MVVM模式的应用,Frame控件在WPF导航中无法适用需求的问题、Prism Region的应用等
首先,我们看下QQ当中对于用户资料查询时的UI设计:
1、加载过程
在这个过程当中,我们的数据是通过服务从服务端请求而来,请求过程存在一定的耗时操作,所以我们通常都会采用异步请求方式,确保UI正常!
2、请求失败的情况
对于这种远程服务请求来说,必然会遇到网络异常或请求失败的情况,或许很多人认为这种几率几乎不存在,或这认为您的应用是内联网应用不需要考虑;
我个人认为作为一个好的设计来说,要尽可能的考虑到所有情况,即便是理论上有可能不存在的情况。
好,上面2则演示我们看到了最简单的对于C/S方式客户端远程请求的一个处理示例;
接下来,我们再考虑一下分布式存在的情况,
如上图展示,我们注意当前窗口中有一个“更新”按钮, 在QQ客户端这个例子当中,我们对于资料查询这个页面来说不会是持久化展示的一个页面,
所以假设该页面中的数据在后台 或者 被其他客户端修改后 , 在当前客户端中并没有自动刷新,QQ的效果是您需要查看的时候手动去点击“更新”
而我们常见的BS系统与C/S系统很大的一个区别在于大多数BS系统的页面跳转频率较高,当每次页面跳转产生后会自动请求数据,所以不需要特别考虑数据一致性问题!(特殊应用系统除外)
如果我们当前的应用是一个监测系统或实时信息显示系统,那么必然要求我们的页面处于持久展示状态,在这样的情况下页面就需要能够及时刷新数据;
在BS当中我们通常通过ajax来异步不断的刷新页面中需要及时展示的数据;那么C/S系统当中如何做呢? 也像ajax一样不断的去异步请求吗?
我个人认为死循环的异步请求并不合适,在移动应用中流量就是一个需要考虑的问题,还有如果数据没有变动,这样的异步请求是否耗费太多的资源!
好,我们就来实际考虑一下如何进行这样一个设计;
首先以一个稍微复杂Client端数据请求与查询为例来实现类似QQ的良好体验:
在这个描述图中,我们的导航区域 与 主内容区域的数据都是从服务端请求来的,类似QQ资料查看窗口(区别在于QQ的导航区域是固定的)
按照前面QQ所展示的,[导航区域]、[主内容区域] 都有可能请求失败,那就要求如果请求失败就要展示“错误页面”,且“错误页面”可以进行<刷新> 重新加载数据;
在上图的描述中我们的[主内容区域]是通过[导航区域]的“导航”后产生的结果,所以它的数据加载应该是由 [导航区域]的 SelectedChanged事件触发;
这样,我们就需要考虑这个“资料查看”窗口应该是分2部分区域,每部分区域展示属于它自己区域的页面,我们来看下在WPF中如何设计这个窗口;
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="150" MaxWidth="200"/>
<ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Frame NavigationUIVisibility="Hidden" Source="/Views/NavigatePage.xaml" Grid.Column="0"/>
<Frame NavigationUIVisibility="Hidden" Source="/Views/MyPage.xaml" x:Name="myContent" Grid.Column="1"/>
</Grid>
通过放置两个Frame控件来承载不同的2个区域的内容页面;
继续,我们考虑下通过选中导航项如何通知[主内容区域] 进行内容加载;既然是导航,那必然涉及到导航参数,这时候我们要考虑如果在2个页面进行参数传递;
我们可以通过选择导航项来控制[主内容区域]的Frame进行Navigate,同时传递参数;
在这里我先通过MVVM模式中由ViewModel通讯来演示如何实现这一步;
class NavigatePageViewModel:ViewModelBase<NavigatePageViewModel> { public NavigatePageViewModel() {m_NavigateCommand = new DelegateCommand<string>(this.NavigateCallback);
}
ICommand m_NavigateCommand = null; public ICommand NavigateCommand { get { return m_NavigateCommand; }}
void NavigateCallback(string args)
{this.SendMessage("Navigate", new NotificationEventArgs(args));
}
}
我们通过导航项绑定Command,再通过Command发送消息的方式来通知内容页面接收参数并加载数据 (注意:这里所说的内容页面 并非内容展示区域,通过MessageBus进行的消息通讯是VM之间的,并不能跨VM与View)
接着我们看下内容展示页面接收到导航参数之后做怎样的操作,
1、注册接收消息(MVVM模式中的应用)
public MyPageViewModel() {this.RegisterToReceiveMessages("Navigate",
new EventHandler<NotificationEventArgs>((sender, e) => {m_ReceiveMessage = e.Message;
this.AsyncLoad();}));
}
我们来分析下段流程; 导航--->发送消息到内容展示页的VM中--->保存导航参数--->加载数据,有什么问题吗?
假设我们[主内容区域]当前展示为“错误页面”,此时我们的主内容页面即便接收到消息并且成功加载了数据,我们的View还是没有正确展示出来,(这里主要的问题是我们使用MVVM模式将逻辑与UI彻底隔离,仅进行数据驱动的结果)
既然我们发现这里的问题,那我们就需要修改下流程: ---->保存导航参数--->让主内容区域显示内容页面--->加载数据 ,这个时候就需要ViewModel通知View了,我们可以使用事件进行通知,
例如:
EventHandler handler = this.Loaded;if (handler != null)
{handler(this, new EventArgs());
}
View页面中接收该事件并执行【让主内容区域展示内容页面】这一逻辑;
private void MyPageViewModel_Loaded(object sender, EventArgs e)
{ var service = NavigationService.GetNavigationService(this);if (service != null)
{ service.Navigate(this);}
}
如果我们使用了Frame作为主内容区域,到这里你就会发现无法获取NavigationService对象,因为当前Frame中存放的是“错误页面”
当然还有另外一个非常重要的问题:Frame在进行导航的时候 每次都会创建新的页面(类似与浏览器每次打开一个连接就会重新请求页面),这时候你就发现你缓存在ViewModel中的数据是没用的,这里你就无法解决上面提到的问题 “设我们[主内容区域]当前展示为“错误页面””
到这里我们就先暂停一下,先不来考虑UI上的问题,我们来讨论一下分布式下如何较好的处理客户端请求数据问题;
在这个图中,我描述了一个简单的发布-订阅 模式,也就是要求我们的客户端需要订阅服务端的一个服务,当服务端自己判断到需要推送数据给客户端的时候它主动进行推送一个消息,这个时候客户端会收到这个消息,然后客户端主动请求服务端刷新数据。(请注意:这里服务端不是主动推送数据给客户端,而是告诉客户端一个消息让它知道自己该刷新数据了)
在《WCF服务编程》一书中,作者已经附带了一个发布-订阅框架,我们可以直接拿来进行使用,经过测试基本能够满足这里我们所说的需求。
这里还有一点需要说明,客户端收到订阅的推送消息后 是主动请求刷新 还是被动请求刷新的问题, 在文章一开始我们看到的QQ的资料查看窗口中,它并没有订阅任何东西,仅仅是最简单的提供一个刷新按钮,
我们需要根据具体的应用场景来设计自己的需求,在QQ的例子中这应该是最好的方式,而在一个实时信息展示版中最好的应该是实时自动刷新,如果在一个即不需要实时刷新又需要持久展示的页面来说怎么办呢?
我认为最好的方式就是,服务端推送消息,客户端收到订阅的消息,同时在UI上提示有新的数据,让用户自己选择手动刷新(因为某些场景下我们不能替用户做主自动去刷新他正在浏览的页面),不知道您同意我的想法吗?
---------------------------------------------------------------------------------------------------------------------------------
好了,关于分布式如何更好的请求与展示数据这里就不多说了,这方面还是比较复杂的,目前我也希望能够学习到更多经验;
我们现在继续来解决前面UI展示的问题,Frame在这里已经不能满足我们的需求了,我们考虑用Prism的Region来操作;
如果你对Prism了解的话,应该知道prism框架是与IOC紧密相连的,涉及到IOC与DI 就会设计到生命周期问题,包括Region区域中的内容页面的生命周期问题;
在Prism框架的应用中,Region区域的内容都是通过 ServiceLocator服务定位器来查找并添加进去的,所以就要求我们的View都配置为 依赖输出项,以MEF为例就是需要View页面标注[Export]
对主窗口的改造很简单,无非是把Frame进行一下替换:
<DockPanel>
<ContentControl MinWidth="100" MaxWidth="200"
DockPanel.Dock="Left" prism:RegionManager.RegionName="NavigationRegion" /> <ContentControl prism:RegionManager.RegionName="ContentRegion"/></DockPanel>
其他的部分也基本与之前提到的相似,比如:
//接收导航参数this.RegisterToReceiveMessages("Navigate",
new EventHandler<NotificationEventArgs>((sender, e) => { //缓存参数m_ReceiveArgs = e.Message;
//通知View已接收到导航参数 EventHandler handler = this.Requested;if (handler != null)
{handler(this, new EventArgs());
}
}));
这样的话,就需要在 内容页面 的View中接收对应的事件做不同的操作,类似如下:
/// <summary> /// 由ViewModel通知自身已接收导航参数 /// </summary> /// <param name="sender"></param> /// <param name="e"></param>private void ContentPageViewModel_Requested(object sender, EventArgs e)
{ //首先确保内容区域为当前View m_RegionManager.RequestNavigate("ContentRegion", "ContentView");}
/// <summary> /// 由ViewModel通知异步数据加载失败 /// </summary> /// <param name="sender"></param> /// <param name="e"></param>private void ContentPageViewModel_Errored(object sender, EventArgs e)
{StringBuilder bulider = new StringBuilder("ErrorView");
var query = new UriQuery();query.Add("ErrorMessage", "模拟加载失败");
bulider.Append(query);
//使内容区域跳转至ErrorPage m_RegionManager.RequestNavigate("ContentRegion",bulider.ToString());
}
public void OnNavigatedTo(NavigationContext navigationContext)
{ContentViewModel viewModel = this.DataContext as ContentViewModel;
if (viewModel != null)
{ //通知ViewModel异步加载数据viewModel.AsyncLoad();
}
}
相对来说,如果加载失败后跳转到了错误页面,那我们只需要让Region 进行返回导航就可以了,那通过Region怎么去导航和管理导航历史?有兴趣的可以去参看Prism框架应用!
实际的使用类似如下:
IRegionNavigationJournal m_Journal = null;public void OnNavigatedTo(NavigationContext navigationContext)
{ //根据参数显示错误信息 m_ErrorMessage = navigationContext.Parameters["ErrorMessage"]; this.NotifyPropertyChanged(m => m.ErrorMessage); //保存Journal对象m_Journal = navigationContext.NavigationService.Journal;
}
private void BackCallback()
{ //注意此处:由于目前是模拟操作,ContentViewModel中已经缓存了 导航参数, //如果缓存的导航参数是Faild,那么这里使用Goback()方法无疑会使主内容区域循环展示为ErrorView //所以此处暂时使用SendMessage给ContentViewModel的方式来模拟使 “重新刷新”生效 //this.SendMessage("Navigate", new NotificationEventArgs("Success"));if (m_Journal != null)
{m_Journal.GoBack();
}
}
以上这部分基本都是关于Prism框架的简单的应用, 需要注意的就是在这里我们的页面与之前我们使用Frame导航有所区别,这里的页面生命周期都是由IOC去控制的,如果没有特别指明,一般都是单例的;
OK,我们还需要考虑一个问题,前面提到了View页面的生命周期问题,如果[导航区域]和[内容区域]的展示都出现错误,那都需要展示错误页面, 我们的错误页面通过导航历史去进行返回的时候怎么办呢?
其实这就是为什么我要提到IOC管理生命周期的原因,我们只需要让ErrorView 也就是错误页面的生命周期模式为 “NonShared” 就可以了,
例如:
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class ErrorView : UserControl
{ public ErrorView() {InitializeComponent();
}
}
有了以上的所有步骤,我们就可以模拟出类似QQ的资料查看窗口的一个用户体验效果了(对Loading\Loaded事件没有添加,有需要完全了解的可以看我的上篇文章中程序示例 点此传送)!!!
(文中所使用的UI技术以及各类框架都只是我们的一种工具,QQ并没有用WPF也没有Prism框架的Region,这里我只是演示一种形式,希望大家能理解我的意思)
最后,结合我们讨论的分布式情况下的客户端数据请求方式,加上这种分区域的利用页面来展示数据的方式,我想大家一定能够设计出比较不错的用户体验;
这里将窗口区域的演示项目上传,希望能帮到有需要的朋友进行学习, 同时非常欢迎与大家讨论C/S模式的分布式开发,很希望能向大家请教经验!
示例项目中需要的第三方框架都是nuget来的,为了保证附件大小没有打包组建,需要组件的自己nuget就可以;
最近熟悉了一下Prism框架,将自己理解的主要知识点分为如下:
1、BootStrapper-启动引导
2、Region-区域
3、Module-模块
4、Aggregate-聚合事件
5、MVVM
根据Prism的QuikStart 以及网上诸多的示例,应用,我们很快便能理解其中的一些知识,并且非常跃跃欲试!
经过一翻学习、运行示例之后…………… 开始了自己的第一个Prism应用;
按照学习到的知识:
首先、我们新建项目、创建一个Shell 并且标记[Export]
[Export]
public partial class Shell : Window
接着、添加BootStrapper类,重写CreateShell()以及InitializeShell方法
public class BootStrapper : MefBootstrapper
{
protected override DependencyObject CreateShell()
{
//注意:Prism的Mef.Container.Catalog的配置来自AggregateCatalog
return this.Container.GetExportedValue<Shell>();
}
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = (Shell)this.Shell;
Application.Current.MainWindow.Show();
}
}
最后、修改App文件通过BootStrapper.Run()来启动应用
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
new BootStrapper()
.Run();
}
}
121、三步走就完成了,原来这么简单哦!!
满怀欣喜的您如果真的这样做了,并且运行、你就会跟我一样杯具的发现在
return this.Container.GetExportedValue<Shell>();这里报错哦~~~ oh my god! 所报错误为MEF无法匹配Export元数据!
为什吗,为什吗呢? 猴急的你也许会跟我一样,重新打开官方示例仔细查看,甚至是copy code过来,也许你还会像我一样往官方示例添加NewShell 试试,或者新建项目添加Shell 并使用官方项目来getExport它!!!!
但是猴急一圈下来,有可能您发现还是错的,如果您运气好,完全copy了官方示例来测试没有出错,那也只能说你不会理解到本文下面将要降到的陷阱!
到这里之后,说实话,我怀疑过我的系统环境,怀疑我新建的项目问题,怀疑过.g.cs的隐藏文件,我甚至拿文本打开项目文件去对比到底有啥差异!
唯一没做的就是去查看prism源码,在google了一翻、苦思冥想了一翻,群里扯了一翻之后,问题还是没有解决的情况下,我开始打开prism的源码一探究竟!
(如果您也经过了这些磨难,甚至认为Prism太操蛋的时候,那赶紧继续往下看吧)
我们需要查看的就是MefBootstrapper类,作为一个菜鸟 ,我们一定要准确快速的定位问题:直接飞至MefBootstrapper类Run方法
this.Logger.Log(Resources.CreatingMefContainer, Category.Debug, Priority.Low);
this.Container = this.CreateContainer(); //方法内部Catalog来自AggregateCatalog
if (this.Container == null)
{
throw new InvalidOperationException(Resources.NullCompositionContainerException);
}
this.Logger.Log(Resources.ConfiguringMefContainer, Category.Debug, Priority.Low);
this.ConfigureContainer(); //内部调用RegisterBootstrapperProvidedTypes注册下列Export
/*
this.Container.ComposeExportedValue<ILoggerFacade>(this.Logger);
this.Container.ComposeExportedValue<IModuleCatalog>(this.ModuleCatalog);
this.Container.ComposeExportedValue<IServiceLocator>(new MefServiceLocatorAdapter(this.Container));
this.Container.ComposeExportedValue<AggregateCatalog>(this.AggregateCatalog);
*/
看看我们重写的方法实实在在的在Run方法里做了一些初始化了,
抓住
this.Container = this.CreateContainer(); //方法内部Catalog来自AggregateCatalog
protected virtual CompositionContainer CreateContainer()
{
CompositionContainer container = new CompositionContainer(this.AggregateCatalog);
return container;
}
这一看,如果您使用过MEF或者对MEF有所了解的话,就能很快想到问题原因了,从一开始我们的报错信息来看,是Export没有匹配到,那就是说MEF.Container中没有包含我们Export出去的东西
为什么没有包含,一:MEF的Catalog有问题;二:Export标记没写(#这废话),看到这里我们就知道了真是Catalog的问题,因为Prism默认的Container首先会从AggregateCatalog添加
那就需要继续往下看看AggregateCatalog到底干了些神马:
老老实实Create
protected virtual AggregateCatalog CreateAggregateCatalog()
{
return new AggregateCatalog();
}
老老实实Config
protected virtual void ConfigureAggregateCatalog() { }
这就奇怪了,既然与AggregateCatalog直接相关的部分没问题,那就要继续往下看:
protected virtual void ConfigureContainer() { this.RegisterBootstrapperProvidedTypes(); }
Hi,有东东哦,go go go继续跟进;
protected virtual void RegisterBootstrapperProvidedTypes()
{
this.Container.ComposeExportedValue<ILoggerFacade>(this.Logger);
this.Container.ComposeExportedValue<IModuleCatalog>(this.ModuleCatalog);
this.Container.ComposeExportedValue<IServiceLocator>(new MefServiceLocatorAdapter(this.Container));
this.Container.ComposeExportedValue<AggregateCatalog>(this.AggregateCatalog);
}
闲话一句,我们发现Prism框架有个依赖组件ServiceLocator ,这是一个非常好的设计哦,建议还不了解同学都去codeplex上看下其源码,和几个扩展实现!
再Okay下,通过上面的简单跟踪知道了Container的Catalog内容之后,我们的问题也清楚了,那就是因为我们Export的程序集未被加载到Container的Catalog中;
在MEF中,Export出的东西叫做部件,存放部件的地方叫做Catalog ,这个Catalog可以是目录,程序集,也可以是代码; 当然MEF的知识还需要单独去了解哦!
同时这里也可以联想到为什么官方示例能够正常运行, 因为大部分示例官方非官方的 他们对Prism框架的演示都包含了Module\Region\Aggregate的应用,
自然他们也就在BootStrapper对Module和Aggregate进行了配置,配置的同时将包含Export的所有程序集都添加到Container中了,
所以在Container获取部件的时候就没有出错,这样想来的话,如果大家一步一步一个功能一个功能实现去学习的话就会遇到陷阱了!
好了, 问题分析清楚了,解决办法就是将自身程序集Catalog注册到Container中去就可以了,
示例:
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
//加载自身
this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(this.GetType().Assembly));
//加载ViewModel所在Assembly
this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(ShellViewModel).Assembly));
//加载Modules目录
if (Directory.Exists("./Modules"))
{
this.AggregateCatalog.Catalogs.Add(new DirectoryCatalog("./Modules"));
}
}
先分享下自己的应用选型, 选择Prism框架的Module\Region\Aggregate功能,当然包含了MEF; 选择SimapleMVVM的MVVM功能;
然后是后话,Prism非常不错的框架,我们现在不管高手、菜鸟 开发的时候都在应用好的设计模式,好的开发方法 和 好的架构;
但是技术更新迅速的同时我们大都是草草了解其使用并直接应用,有时候可能没有完全理解其精粹;拿prism来说,作为企业开发框架,主打功能就是解耦、解耦的方法就是依赖注入,再一个就是设计模式;
我们使用prism的完全可以将项目拆分为很多小的工作模块去开发, 目前我的项目中就这样,UI就是纯UI ViewModel单独项目且Export 并且将程序集添加到Prism的Container供UI层去Import
然后ViewModel在通过 IService从Core模块中获取服务,
然后利用ServiceLocator 或者 prism 在任意地方去Import 就可以做很多工作了, 之前做WPF都是将ViewModel在View.xaml中去绑定或者写到资源里,
现在我更喜欢这样:
[Import]
public ShellViewModel ViewModel
{
get
{
return (ShellViewModel)this.DataContext;
}
set
{
this.DataContext = value;
}
}
结尾:对Prism的学习和应用才刚开始,有新的理解和问题我会继续和大家分享 ;另外我现在的项目为C/S分布式应用,服务端EF+WCF+WindowsService, 客户端WPF+WCF ,设计过程和解决问题的过程中也有很多有意思的东西,有时间的话我也会分享出来,供朋友们学习交流!
名词解释:此动态非运行时动态,让EF动态支持新增表、动态切换数据库意在不改变项目核心框架,
通过新增或者替换组件的方式达到标题目地。
一、先来点简单的,动态支持多数据库
AppDbContext实现:
public class AppDbContext:DbContext
{
public AppDbContext(string configKey)
: base(configKey)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
在AppDbContext构造函数中添加configKey参数,通过configKey参数指定配置文件中的连接字符串的配置项;
创建IDbContextProvider接口,如下:
public interface IDbContextProvider
{
AppDbContext Get();
}
意图很明显了,通过IDbContextProvider来提供AppDbContext,这样我们首先将AppDbContext与业务层解耦;
继续创建2个项目:MsSqlProvider、MySqlProvider,分别实现IDbContextProvider接口:
MsSql:
public class MsSqlProvider:IDbContextProvider
{
AppDbContext m_AppDbContext = null;
public AppDbContext Get()
{
return m_AppDbContext ?? new AppDbContext("MsSql");
}
}
MySql:
public class MySqlProvider:IDbContextProvider
{
AppDbContext m_AppDbContext = null;
public AppDbContext Get()
{
return m_AppDbContext ?? new AppDbContext("MySql");
}
}
下面继续解释动态支持/切换DbContextProvider,没错…聪明的你一开始就应该想到了..依赖注入,这个时候我们就需要使用依赖注入来完成使命了;
我已MEF为例来演示下如何动态获取2种DbContextProvider:
首先为我们的IDbContextProvider添加 [InheritedExport] 标记,然后分别为两种Provider添加 [Export]标记;
"MEF的使用还请大家自己去熟悉,我也仅仅是会使用而已,并不精通"
接着在Demo中添加App.Config和测试代码;
App.Config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="MsSql" connectionString="Data Source=LIANG-HU-PC;Initial Catalog=appbase;Integrated Security=True;Pooling=False" providerName="System.Data.SqlClient" />
<add name="MySql" connectionString="server=localhost;User Id=root;password=mysql;Persist Security Info=True;database=appbase" providerName="MySql.Data.MySqlClient" />
</connectionStrings>
</configuration>
这里要提醒下哦:要使MySql能够支持EF使用的话,需要到MySql官方下载最新的驱动;
测试代码如下:
class Program
{
[ImportMany]
static IEnumerable<IDbContextProvider> m_Providers = null;
static void Main(string[] args)
{
//使用目录方式查找MEF部件
var catalog = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory);
//创建Container
var container = new CompositionContainer(catalog);
//获取Export项,这里直接加载不采用Lazy
m_Providers = container.GetExportedValues<IDbContextProvider>();
if (m_Providers != null)
{
foreach (var provider in m_Providers)
{
Console.WriteLine(provider.Get().Database.Connection.ConnectionString);
}
}
Console.ReadKey(false);
}
OK,我们来编译测试下,当应用程序目录下没有任何Provider的时候是没有获取到任何是不会获取到任何Provider的,如果只放置MySqlProvider再执行的话结果如下:
放置两项Provider组件的时候自然就会是两个都被获取到,我就不演示了;
到这里可能很多人就要嘘声一片了,也许你会提出一些问题:
比如:
1)为什么不做一个Provider的实现,在Get()方法或者构造函数中依赖注入参数呢?
其实这样做的目地是我们在使用UnitOfWork和Repository模式时能够简单方便的获取DbContext;
可参见示例:
/// <summary>
/// Entity Framework Repository
/// </summary>
/// <typeparam name="T"></typeparam>
public class EFRepository<T>:IRepository<T>
where T:class
{
readonly IDataBaseFactory m_DataBaseFactory = null;
public EFRepository(IDataBaseFactory dataBaseFactory)
{
if(dataBaseFactory==null)
{
throw new ArgumentNullException("DataBaseFactory");
}
m_DataBaseFactory=dataBaseFactory;
}
DbContext m_DbContext = null;
protected DbContext DbContext
{
get
{
return m_DbContext ?? m_DataBaseFactory.Get();
}
}
}
对UnitOfWork模式的使用与此类似;
2)我只需要一个DbContext,但有时候需要切换数据库,那怎么办呢?
这个问题是ico与依赖注入方面的基础内容,需要您自己去学习哦;
至此,简单的“动态”支持多数据库示例就完成了~~~ 我们的关键还是动态支持新建表,下面我们就来一步一步实践吧;
二、“动态”支持新建表,计划先行
首先我们创建ModelBase类库,存放一些与实体相关的接口和基类,结构如图所示:
IEntity接口与AbstractEntityBase类,顾名思义,大家应该猜得到它们是实体基类,为什么要如此定义呢,主要是方便我们写实体的时候直接继承Id属性,(因为我们的所有表主键都是Guid且名为Id)
public interface IEntity
{
Guid Id { get; }
}
public abstract class AbstractEntityBase : IEntity
{
public AbstractEntityBase()
{
this.Id = Guid.NewGuid();
}
[Key]
[Required]
public Guid Id
{
get;
protected set;
}
}
还有一个好处就是我们直接在基类中描述 主键关系,在写实体的时候直接继承后,可以省去很多重复操作哦^_^
再来说IMapping和Mapping,为什么要有这2个基类接口呢,出于以下方面考虑:
1)将实体与数据库的映射关系产生Mapping类与DbContext类解耦(这个会在下面具体出现时再说)
2)通过MappingBase基类实现一些公共操作,避免每个实体类的重复操作,具体看代码你就会明白;
[InheritedExport]
public interface IMapping
{
void RegistTo(ConfigurationRegistrar configurationRegistrar);
}
public class MappingBase<TEntity> : EntityTypeConfiguration<TEntity>, IMapping
where TEntity : class,IEntity
{
public MappingBase()
{
this.Map(m => m.ToTable(typeof(TEntity).Name));
}
public void RegistTo(ConfigurationRegistrar configurationRegistrar)
{
configurationRegistrar.Add(this);
}
}
呵呵,有了“动态”支持多数据库,这里很多人应该就能猜到我们如何“动态”支持新增表咯;注意这里的IMapping接口的精妙所在哦,您发现了吗???;
三、万事俱备,只欠东风
我们先在ModelA类库中创建一个User实体和Role实体,同时创建UserMapping和RoleMapping,(为什么要创建Mapping类,后面我会讲)
USer 、UserMapping:
/*
* 为什么没有通过[Table]来指明表明呢,
* 并不是因为我们需要EF自己支持的表明方式
* 而是我们继承自AbstractEntityBase,在其基类已经实现了将类名映射为表名
*/
public class User : AbstractEntityBase
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
/*
* 注意这里,我为什么不通过DataAnnotations方式添加外键关联呢
* 个人认为User实体与Role实体关联,已经拥有Role属性了,
* 如果在添加一个RoleId来表示外键关系,会让我觉得User类不够清爽
* 所以我的做法是添加UserMapping类来指定它与Role实体的关系
*
* 但是有一点要注意,如果不指定外键的话,默认数据库外键是为 表名_主键(Role_Id)类型
*/
public virtual Role Role{get;set;}
}
[Export("UserMapping")]
public class UserMapping:MappingBase<User>
{
public UserMapping()
{
this.HasRequired(m => m.Role)
.WithMany(m => m.Users);
/*注意这里没有指定HasForeignKey哦*/
}
}
Role类和RoleMapping的实现也是同理,结合上面代码中的注释内容,我想大家也能够理解我的良苦用心了吧;如果还不能理解,我们再看下DbContext是如何实现的:
/*
* 很清爽的DbContext,完全不包含任何DbSet
* 通过Mapping来加载表结构
*/
public class AppDbContext:DbContext
{
public AppDbContext(string configKey)
: base(configKey)
{
//可以设置通过反向方式创建表哦,但是我们演示的目地不在于此
//Database.SetInitializer(new DropCreateDatabaseIfModelChanges<AppDbContext>());
//加载目录下所有IMapping实现
var catalog = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory);
var container = new CompositionContainer(catalog);
m_Mappings = container.GetExportedValues<IMapping>();
}
[ImportMany]
IEnumerable<IMapping> m_Mappings = null;
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
if (m_Mappings != null)
{
//这里是关键
foreach (var mapping in m_Mappings)
{
mapping.RegistTo(modelBuilder.Configurations);
}
}
base.OnModelCreating(modelBuilder);
}
}
没错,我们的目地就是让DbContext完全依靠IMapping去加载解释表结构关系,这样即保证DbContext不包含大量的DbSet,又能够非常好的将DbContext与实体解耦,
更重要的是,我们通过 DataAnnotations 和 Fluent API结合使用,让我们的实体也非常清爽;
到这里,其实我已经把核心的内容都展现出来了,对于新增表的动态使用也就类似与最前面讲的“动态”支持多数据库,我们只需要依赖注入所有的IMapping的实现,就可以让DbContext自动去解释所有表结构了(所以DbContext的OnModelCreating方法是关键所在)。
好,接着我们在新增一个ModelB 作为新增表NewModel实体的载体,来演示是我们的示例是否能够如题所描述的那样,不改变核心框架的前提下动态支持新增的表和实体。
public class NewModel : AbstractEntityBase
{
[Required]
public string Name { get; set; }
}
我们按照之前做CmdDemo的方式添加一个AppDemo,并添加App.Config文件,同时创建一个DataViewControl的自定义控件用来显示数据;
我们来看下AppDemo的演示:
其具体实现为:
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
IDbContextProvider provider = new MsSqlProvider.MsSqlProvider();
AppDbContext dbContext = provider.Get();
var users = dbContext.Set<User>();
if (users != null)
{
users.Add(new User()
{
Username = "admin",
Password = "admin",
Role = new Role() { Name = "administrators" }
});
dbContext.SaveChanges();
DataViewControl usersViewControl=new DataViewControl();
usersViewControl.Binding(users.ToList());
TabItem item = new TabItem();
item.Header = "User表展示";
item.Content = usersViewControl;
this.myTabControl.Items.Add(item);
}
var roles = dbContext.Set<Role>();
if (roles != null)
{
DataViewControl rolesViewControl = new DataViewControl();
rolesViewControl.Binding(roles.ToList());
TabItem item = new TabItem();
item.Header = "Role表展示";
item.Content = rolesViewControl;
this.myTabControl.Items.Add(item);
}
/*
* 请注意此处,我们的NewModel还是和应用耦合在一起了,
* 并没有像我们标题说的动态加载;
* 这里主要是为了演示方便,我就不在做实体与业务层的解耦了,
* 一般我们的应用可能是单独的UI模块和它对应的实体耦合,而不是UI框架耦合
* 仅在需要的时候加载不同模块的UI组件
*
*/
var newModels = dbContext.Set<NewModel>();
if (newModels != null)
{
DataViewControl newModelsViewControl = new DataViewControl();
newModelsViewControl.Binding(newModels.ToList());
TabItem item = new TabItem();
item.Header = "NewModel表展示";
item.Content = newModelsViewControl;
this.myTabControl.Items.Add(item);
}
}
}
需要解释的是AppDemo中没有很好的演示怎么动态支持新建表,其实我前面解释过ModelB中NewModel就是新增的表,主要是为了给大家展示实现思路,我并没有去把NewModel和AppDemo去解耦,所以没有很好的演示效果,但是实际上是没有问题的,这就跟我们具体的应用息息相关了。
到这里,我们已经完整的解释了整个过程,为此我也是边创建项目边写博客,在最后会附上完整项目源码,有兴趣的可以自行下载学习;如果这篇文章对你有所启发,或者让你学到了一些东西,那是我非常乐见的,同时也希望各位高手不要鄙视。







