【WPF】请注意一个 异步绑定 的陷阱!
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;
}
}
}
现在你还会发现任何问题吗?
WPF技巧(1)异步绑定
与大家共勉
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
WPF调用WCF不支持异步,但是在加载数据的时候阻止UI是很影响UX的。这里自己实现了一下异步调用:
WCF部分的接口实现就不讨论了,建立一个普通的返回数据的服务即可。
这里我 建一个WCF服务:CoreService,
建立一个接口(GetUserInfoByAliasAndPassword)
返回用户信息(UserInfo)
1. 首先在WPF客户端实例化一个服务:
CoreService.CoreServiceClient client = new CoreService.CoreServiceClient();
2. 然后在需要调用服务端地方开始实施异步调用,为了可以拿到返回值,我用的是Func
Func
(delegate(string s1, string s2){...调用wcf服务接口并返回...});
或
Func
((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 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、客户端异步调用服务
客户端异步调用服务主要方式有几种:
测试代码如下:
{ Console.WriteLine(i); } int resul = calculatorClient.EndAdd(asyncResult); Console.WriteLine(string.Format("计算结果:{0}",resul));
{ for (int i = 0; i < 20; i++) { Console.WriteLine(i); } Console.WriteLine("开始计算..."); return x + y; }
客户端输出如下:
{ Console.WriteLine(i); } Thread.Sleep(5000); Console.WriteLine("开始计算..."); return x + y;
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});
这种方式是对服务的异步调用完成以后,自动调用回调来获取结果。
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); }
将服务定义为异步服务,契约定义如下 :
IAsyncResult BeginCalculator (int x,int y ,AsyncCallback asyncCallback, object state);
void EndCalculator(IAsyncResult);
然后在实现契约接口的服务中,将方法实现为异步的。