WPF 异步(转)



【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传递的只是一个stringUI 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.

WPFUI 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 myFunc = new Func
                (delegate(string s1, string s2){...调用wcf服务接口并返回...});
      或
     Func myFunc = new 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 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中通讯的双方来说,客户端可以异步的调用服务;服务端对服务也能以异步的方式实现。这就涉及到两个方面:WCF客户端异步调用服务;服务端的异步实现本节是Artech《WCF技术剖析(卷1)》一篇读书笔记。在文章中老A介绍了客户端如何以异步的方式消费WCF服务,以及服务的异步实现,个人对这几种方式做个总结。

 

目录:
  1. WCF客户端异步调用服务
  2. 服务端的异步实现 

 

WCF客户端异步调用服务主要通过生成异步的代理类,然后调用其中的异步方法来实现异步调用。
异步代理类的生成:
  • 通过SvcUtil  /async   直接生产异步代理;
  • 通过添加应用的方式,点击”添加引用“的“高级”按钮,在弹出来的对话框中选择“生成异步”。如图:
WPF 异步(转)_第1张图片

 

生成的异步调用代理类部分借口:

        [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;     }

服务端输出如下:WPF 异步(转)_第2张图片

 客户端输出如下:

WPF 异步(转)_第3张图片 

这种方式使用了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( 12

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(1036new[] { 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最为返回值,这两者矛盾的。

你可能感兴趣的:(WPF 异步(转))