这段代码贴入VS2010以_wpf asyncstate">
赞
踩
WPF的异步绑定在很大程度上简化了延迟较大的绑定操作,大大提高了生产力。但在这个糖果里存在一个天大的陷阱。
如果你不能发现这个陷阱,轻则导致延迟严重,重则导致程序崩溃。
请看第一个案例:
<Image Source="http://images.cnblogs.com/adminlogo.gif" />
这段代码贴入VS2010以后,第一反应就是VS会假死一段时间。然后在Design窗口才会显示出园子的logo。运行段代码,程序也要假死一段时间才能正常工作。原因就是通过网络加载这个图片的时间过长。在我这需要10+秒。
看到这里,你一定会说,使用异步操作就可以了。
请看第二个案例:
前台代码:
<Image Source="{Binding IsAsync=True}"/>
后台代码:
public MainWindow()
{
InitializeComponent();
this.DataContext = "http://images.cnblogs.com/adminlogo.gif";
}
看上去这段代码能够完美的工作,可实际上效果和案例一完全一样。除了不会让VS假死一段时间以外。
原因是因为你给UI Thread传递的只是一个string,UI Thread调用Async Thread把它转化为Uri以后,继续用UI Thread去下载图片,自然还是会导致阻塞。
那么自然就会想起第三种方案。
请看第三个案例:
前台代码:
<Image Source="{Binding Image, IsAsync=True}"/>
后台代码:
public MainWindow()
{
InitializeComponent();
this.DataContext = new VM();
}
public class VM
{
public ImageSource Image
{
get
{
return new BitmapImage(new Uri("http://images.cnblogs.com/adminlogo.gif"));
}
}
}
这样的代码看上去绝对是完美了。
运行,并没有发现阻塞了UI Thread,正等待图片异步下载完毕的时候,突然弹出异常:
The calling thread cannot access this object because a different thread owns it.
在WPF中UI Thread是不能使用非UI Thread创建的Image的。
下面正式推出我们的终极解决方案:
前台代码不变,后台代码修改为
public MainWindow()
{
InitializeComponent();
this.DataContext = new VM(this.Dispatcher);
}
public class VM
{
Dispatcher _dispatcher = null;
public VM(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public ImageSource Image
{
get
{
if (_dispatcher == null)
return null;
BitmapImage bi = null;
try
{
WebClient wc = new WebClient();
MemoryStream ms = new MemoryStream(wc.DownloadData("http://images.cnblogs.com/adminlogo.gif"));
_dispatcher.Invoke(new Action(() =>
{
bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.EndInit();
}), null);
}
catch { }
return bi;
}
}
}
现在你还会发现任何问题吗?
与大家共勉
1.定义一个集合属性
private IList<string> _list; public IList<string> List { get { if (_list == null) { _list = new List<string>(); for (int i = 0; i < 100; i++) { _list.Add("item" + i); } } return _list; } }
2.绑定属性
<Window x:Class="WpfRecipe1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfRecipe1" Title="MainWindow" Height="350" Width="525"> <Grid> <ListBox ItemsSource="{Binding List,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:MainWindow}}}" Height="179" HorizontalAlignment="Left" Margin="76,31,0,0" Name="listBox1" VerticalAlignment="Top" Width="135" /> </Grid> </Window>
上面的代码可以很好的运行.
事实上我们取数据都没这么简单,假设这段数据是从数据里取的,花费3秒时间,我们以线程模拟
private IList<string> _list; public IList<string> List { get { System.Threading.Thread.Sleep(3000); //省略代码 return _list; } }
下面重新运行代码,你将会发现程序会先停滞三秒.结论在刚开始已经提到
即使下面的代码也是遵循上面的原则
<TabControl> <TabItem Header="tab1"></TabItem> <TabItem Header="tab2"> <ListBox ItemsSource="{Binding List,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:MainWindow}}}" Height="179" HorizontalAlignment="Left" Margin="76,31,0,0" Name="listBox1" VerticalAlignment="Top" Width="135" /> </TabItem> </TabControl>
这是相当郁闷的一段代码,因为用户什么也没看到,却让用户等了三分钟.
首先想到的是自己异步取数据,其实还有一个更简单的方法就是在绑定时将IsAsync设置为True
<ListBox ItemsSource="{Binding List, IsAsync=True, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:MainWindow}}}" Height="179" HorizontalAlignment="Left" Margin="76,31,0,0" Name="listBox1" VerticalAlignment="Top" Width="135" />
说实话我也刚刚知道有这么一个属性,这个属性几乎被我们忽略,平时用到很少,因为我们第一思维就是自己异步去加载数据,学了一招
WPF调用WCF不支持异步,但是在加载数据的时候阻止UI是很影响UX的。这里自己实现了一下异步调用:
WCF部分的接口实现就不讨论了,建立一个普通的返回数据的服务即可。
这里我 建一个WCF服务:CoreService,
建立一个接口(GetUserInfoByAliasAndPassword)
返回用户信息(UserInfo)
1. 首先在WPF客户端实例化一个服务:
CoreService.CoreServiceClient client = new CoreService.CoreServiceClient();
2. 然后在需要调用服务端地方开始实施异步调用,为了可以拿到返回值,我用的是Func
Func<string, string, UserInfo> myFunc = new Func<string, string, UserInfo>
(delegate(string s1, string s2){...调用wcf服务接口并返回...});
或
Func<string, string, UserInfo> myFunc = new Func<string, string, UserInfo>
((string s1, string s2) =>{...调用wcf服务接口并返回...});
这里用的方法比较懒,不想再去定义代理了就用了匿名的 呵呵
3. 接下来就是执行异步调用并拿到返回值了,用Func的BeginInvoke
myFunc.BeginInvoke("arg1", "arg2", callBack, object);
拿数据的方法是执行myFunc.EndInvoke,它的返回值就是在我们的WCF服务接口返回的数据了,这个数据的类型当然是我们在定义Func时定义好的
类型了 呵呵
有两点要注意:
A. 这个EndInvoke一定要保证在BeginInvoke的callBack里调用呀 这个大家应该都明白,等服务接口执行完了才有数据可以拿呀 呵呵
B. 拿回来数据后如果要操作WPF的UI,可别在callBack里直接搞啊,因为callBack执行的时候已经不是和UI在同一个线程了,要用UI线程的
Dispatcher.BeginInvoke()..
以下是完整代码:
public MainWindow() { InitializeComponent(); this .Loaded += new RoutedEventHandler(MainWindow_Loaded); } void MainWindow_Loaded( object sender, RoutedEventArgs e) { // Func定义 参数:用户名,密码, 返回值 Func < string , string , UserInfo > myFunc = new Func < string , string , UserInfo > ( delegate ( string s1, string s2) // Func myFunc = new Func((string s1, string s2) => { // 调用wcf服务接口并返回 CoreService.CoreServiceClient client = new CoreService.CoreServiceClient(); UserInfo ui = client.GetUserInfoByAliasAndPassword(s1, s2); return ui; }); IAsyncResult result = myFunc.BeginInvoke( " jing " , " 123 " , (res) => { UserInfo user = myFunc.EndInvoke(res); show( " >> " , user.Alias.ToString(), user.Name); }, null ); } void show( params string [] str) { this .Dispatcher.BeginInvoke( new Action(() => { StringBuilder sb = new StringBuilder(); str.ToList().ForEach(s => sb.Append(s + " , " )); this .Content = new TextBlock() { Text = sb.ToString().Substring( 0 , sb.ToString().Length - 2 ) }; }) ); }对于WCF中通讯的双方来说,客户端可以异步的调用服务;服务端对服务也能以异步的方式实现。这就涉及到两个方面:WCF客户端异步调用服务;服务端的异步实现本节是Artech《WCF技术剖析(卷1)》一篇读书笔记。在文章中老A介绍了客户端如何以异步的方式消费WCF服务,以及服务的异步实现,个人对这几种方式做个总结。
WCF客户端异步调用服务主要通过生成异步的代理类,然后调用其中的异步方法来实现异步调用。异步代理类的生成:
- 通过SvcUtil /async 直接生产异步代理;
- 通过添加应用的方式,点击”添加引用“的“高级”按钮,在弹出来的对话框中选择“生成异步”。如图:
![]()
生成的异步调用代理类部分借口:
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]public System.IAsyncResult BeginAdd(int x, int y, System.AsyncCallback callback, object asyncState) { return base.Channel.BeginAdd(x, y, callback, asyncState); } public void AddAsync(int x, int y) { this.AddAsync(x, y, null); } public void AddAsync(int x, int y, object userState) { if ((this.onBeginAddDelegate == null)) { this.onBeginAddDelegate = new BeginOperationDelegate(this.OnBeginAdd); } if ((this.onEndAddDelegate == null)) { this.onEndAddDelegate = new EndOperationDelegate(this.OnEndAdd); } if ((this.onAddCompletedDelegate == null)) { this.onAddCompletedDelegate = new System.Threading.SendOrPostCallback(this.OnAddCompleted); } base.InvokeAsync(this.onBeginAddDelegate, new object[] { x, y}, this.onEndAddDelegate, this.onAddCompletedDelegate, userState); }
1、客户端异步调用服务
客户端异步调用服务主要方式有几种:
1.1、直接调用异步方法:在生成的代理类中,有BeginAdd\EndAdd等服务契约中定义的Add操作的异步实现。直接调用BeginAdd方法,实现客户端异步调用服务端方法。在调用BeginAdd方法后,可执行一些其他操作,这些操作与服务端Add调用并行执行,Add调用最终通过EndAdd方法得到。测试代码如下:
客户端代码:IAsyncResult asyncResult = calculatorClient.BeginAdd( 1 , for ( int i = 0 ; i < 10 ; i++){ Console.WriteLine(i); } int resul = calculatorClient.EndAdd(asyncResult); Console.WriteLine(string.Format("计算结果:{0}",resul));
服务端代码:public int Add( int x, int y){ for (int i = 0; i < 20; i++) { Console.WriteLine(i); } Console.WriteLine("开始计算..."); return x + y; }
服务端输出如下:
客户端输出如下:
这种方式使用了EndAdd方法,如果服务端没有执行完成,当前线程会被阻塞直到异步调用的服务完成后结束。如客户端代码保持不变,将服务实现改为如下:for ( int i = 0 ; i < 20 ; i++){ Console.WriteLine(i); } Thread.Sleep(5000); Console.WriteLine("开始计算..."); return x + y;
如果在服务端让线程睡眠几秒,就可看到客户端会被阻塞:1.2、通过回调的方式异步调用服务:在生成的异步调用代理类中,还可以通过回调用服务;IAsyncResult asyncResult = calculatorClient.BeginAdd( 1, 2,delegate(IAsyncResult asyncResult) { int [] array = asyncResult.AsyncState as int []; int result= calculatorClient.EndAdd(asyncResult1); calculatorClient.close() Console.WriteLine(string.Format("{0}+{1}={2}", array[0], array[1], result)); }, new []{1,2});
这种方式是对服务的异步调用完成以后,自动调用回调来获取结果。
1.3、通过为异步操作注册事件//进行异步调用calculatorClient.AddAsync(10, 36, new[] { 1000 });
//为异步调用完成定义触发事件
calculatorClient.AddCompleted += calculatorClient_AddCompleted; Console.WriteLine("服务调用完成..."); Console.ReadKey();
//异步调用完成后执行
privatestaticvoid calculatorClient_AddCompleted(object obj, AddCompletedEventArgs args) { var array = args.UserState as int[]; int result = args.Result; Console.WriteLine(result); }
2、服务的异步实现:将服务实现定义成异步的方式,需要将OperationContract的AsyncPattern设置为true;应用到BeginXX接口上,并且此操作的最后两个参数必须为AsyncCallback ,object;需要有一个EndXX(IAsyncResult asyncResult)的接口与异步调用的接口匹配。需要注意的是EndXX(IAsyncResult asyncResult)不能再次声明为契约接口,也就是不能再标记为OperationContract。将服务定义为异步服务,契约定义如下 :
[OperationContract(AsyncPattern = true )]IAsyncResult BeginCalculator (int x,int y ,AsyncCallback asyncCallback, object state);
void EndCalculator(IAsyncResult);
然后在实现契约接口的服务中,将方法实现为异步的。
将契约接口声明为单向,也就是OneWay,这样客户端对此服务接口调用就是异步的。因为它无需等待服务端返回,客户端只需将消息发送到传输层就立即返回。那能不能将将这种客户端对服务端的异步调用直接标记为单向的,那是不是可以不用生成异步代理类(即上述SvcUtil /Ayncs或者在添加引用时声明将代理类生成异步操作)?答案是否定的,因为OneWay要求方法的返回值为void,而异步的方法需要IAsyncResult最为返回值,这两者矛盾的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。