PDC 10 – The C#、VB.NET Future: Visual Studio Async CTP

在C# 4.0、VB.NET 2010時,C#的主要發明者Anders Hejisberg 宣布這兩個程式語言將進入Dynamic Programming的時代,以dynamic(C#)、Dim(VB.NET 2010)兩個宣告式開啟
了Dynamic Programming時代,從此之後,C#、VB.NET 2010在使用Automation(COM)及其他無法於編譯時期得知型別及成員函式為何時,能更加的簡單即快速來呼叫它們。
在PDC 10中,Anders再次宣告了另一個時代的來臨,C#及VB.NET將攜手進入Async Programming時代,在這個時代中,C#與VB.NET在使用非同步呼叫等相關函式時,將更加的簡便,
更加地趨近於同步呼叫模式。

PDC 10 – The C#、VB.NET Future: Visual Studio Async CTP

 

 

/黃忠成

 

 

C#、VB.NET 語言的變革

 

        在C# 4.0、VB.NET 2010時,C#的主要發明者Anders Hejisberg 宣布這兩個程式語言將進入Dynamic Programming的時代,以dynamic(C#)、Dim(VB.NET 2010)兩個宣告式開啟

了Dynamic Programming時代,從此之後,C#、VB.NET 2010在使用Automation(COM)及其他無法於編譯時期得知型別及成員函式為何時,能更加的簡單即快速來呼叫它們。

      在PDC 10中,Anders再次宣告了另一個時代的來臨,C#及VB.NET將攜手進入Async Programming時代,在這個時代中,C#與VB.NET在使用非同步呼叫等相關函式時,將更加的簡便,

更加地趨近於同步呼叫模式。

 

 

Async-Programming Style

 

       那什麼是Async Programming呢?說穿了,就是把一個函式呼叫動作放到一個執行緒中執行,使其不會影響到主程式的運行,舉例來說,當你下達一個SQL查詢時,在同步程式寫法中,

得先透過ADO.NET下達SQL指令,接著就等待著其返回查詢結果,在這期間,程式將處於無回應狀態,因為它正在等待著查詢結果的回傳。

     當使用非同步寫法時,我們會將下達SQL指令的動作放到執行緒中,將等待查詢結果的工作交給執行緒,當收到查詢結果時,執行緒會將查詢結果送往另一個函式進行後續處理,在這種寫法中,

查詢的動作不會導致UI的停滯。

     非同步的寫法在Silverlight中相當的常見,由於其先天的設計,使得Silverlight應用程式的所有網路動作都必須是以非同步方式完成的,舉一個例子,我們在Server端有個pics.txt,裡面內容為數個

圖形檔案名稱,如下所示:

pics.txt

Chrysanthemum.jpg

Desert.jpg

Hydrangeas.jpg

Jellyfish.jpg

Koala.jpg

Lighthouse.jpg

Penguins.jpg

 

我們的Silverlight應用程式任務,就是下載這個文字檔,依據其內容來下載圖形檔案並顯示,一般來說會寫成下面這樣:

 

MainPage.xaml

<UserControlx:Class="AsyncPictureShow.MainPage"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    mc:Ignorable="d"Height="396"Width="603">

 

    <Gridx:Name="LayoutRoot"Background="White">

        <ScrollViewerHeight="349"HorizontalAlignment="Left"Margin="12,12,0,0"

                             Name="scrollViewer1"VerticalAlignment="Top"Width="343" >

            <StackPanelName="picPanel"Orientation="Vertical">

               

            </StackPanel>

        </ScrollViewer>

        <ButtonContent="Download"Height="23"HorizontalAlignment="Left"Margin="464,322,0,0"Name="button1"

                        VerticalAlignment="Top"    Width="75"Click="button1_Click" />

    </Grid>

</UserControl>

MainPage.xaml.cs

using  System;

using  System.Collections.Generic;

using   System.Linq;

using   System.Net;

using   System.Windows;

using   System.Windows.Controls;

using   System.Windows.Documents;

using  System.Windows.Input;

using   System.Windows.Media;

using   System.Windows.Media.Animation;

using   System.Windows.Shapes;

using   System.Windows.Media.Imaging;

using   System.IO;

 

namespace   AsyncPictureShow

{

    public partial class MainPage : UserControl

    {

        public MainPage()

        {

            InitializeComponent();

        }

 

        private void button1_Click(object sender, RoutedEventArgs e)

        {

            WebClient client = new WebClient();

            client.DownloadStringCompleted += (s, args) =>

                {

                    using(StringReader sr = new StringReader(args.Result))

                    {

                        while(sr.Peek() != -1)

                        {

                            string imgFile = sr.ReadLine();

                            Image img = new Image();

                            WebClient client2 = new WebClient();

                            client2.OpenReadCompleted += (s1,args1)=>

                                {

                                    BitmapImage bmp = new BitmapImage();

                                    bmp.SetSource(args1.Result);

                                    img.Source = bmp;

                                    picPanel.Children.Add(img);

                                };

                            client2.OpenReadAsync(new Uri("../" + imgFile,

                                                                                                 UriKind.Relative));

                        }

                    }

                };

            client.DownloadStringAsync(new Uri("../pics.txt", UriKind.Relative));

        }

    }

}

 

其結果如圖001。

圖001

     結果很正常,但如果多執行幾次,你應該會發現,圖形的顯示順序並未依照pics.txt中的順序,這是因為OpenReadCompleted是不會依照呼叫OpenReadAsync的順序來觸發的,

這是非同步寫法的特色,哪一個OpenReadAsync先完成下載動作,誰就先觸發OpenReadCompleted,與你呼叫OpenReadAsync的順序無關。

如果想讓這個程式照著pics.txt的順序來顯示圖形,那麼得改變寫法,用上遞迴技術才行。

MainPage.xaml.cs

         private   void LoadImage(List<string> list, int index)

         {

            if (list.Count == index)

                return;

            WebClient client = new WebClient();

            client.OpenReadCompleted += (s, args) =>

                {

                    Image img = new Image();

                    BitmapImage bmp = new BitmapImage();

                    bmp.SetSource(args.Result);

                    img.Source = bmp;

                    picPanel.Children.Add(img);

                    LoadImage(list, ++index);

                };

            client.OpenReadAsync(new Uri("../" + list[index], UriKind.Relative));

        }

 

        private void button1_Click(object sender, RoutedEventArgs e)

        {

             WebClient client = new WebClient();

             client.DownloadStringCompleted += (s, args) =>

                 {

                     if (args.Error != null)

                     {

                         MessageBox.Show(args.Error.InnerException.Message);

                         return;

                     }

 

                     List<string> list = new List<string>();

                     using (StringReader sr = new StringReader(args.Result))

                     {

                         while (sr.Peek() != -1)

                             list.Add(sr.ReadLine());

                     }

 

                     LoadImage(list, 0);

                 };

 

             client.DownloadStringAsync(new Uri("../pics.txt", UriKind.Relative));

}

 

問題還不只於此,如果要為這段程式碼加上錯誤控制,那麼就得寫成下面這樣。

 

MainPage.xaml.cs

private  void LoadImage(List<string> list, int index)

 {

            if (list.Count == index)

                return;

            WebClient client = new WebClient();

            client.OpenReadCompleted += (s, args) =>

                {

                    if (args.Error != null)

                    {

                        MessageBox.Show(args.Error.Message);

                        return;

                    }

                    Image img = new Image();

                    BitmapImage bmp = new BitmapImage();

                    bmp.SetSource(args.Result);

                    img.Source = bmp;

                    picPanel.Children.Add(img);

                    LoadImage(list, ++index);

                };

            client.OpenReadAsync(new Uri("../" + list[index], UriKind.Relative));

 }

 

private  void button1_Click(object sender, RoutedEventArgs e)

{

             WebClient client = new WebClient();

             client.DownloadStringCompleted += (s, args) =>

                 {

                     if (args.Error != null)

                     {

                         MessageBox.Show(args.Error.Message);

                         return;

                     }

 

                     List<string> list = new List<string>();

                     using (StringReader sr = new StringReader(args.Result))

                     {

                         while (sr.Peek() != -1)

                             list.Add(sr.ReadLine());

                     }

 

                     LoadImage(list, 0);

                 };

 

             client.DownloadStringAsync(new Uri("../pics.txt", UriKind.Relative));

}

 

想更完美些,在使用者按下這個Button時,將其設為不可用(IsEnabled = false)狀態,就需要再進行處理。

 

MainPage.xaml.cs

private   void LoadImage(List<string> list, int index)

{

            if (list.Count == index)

            {

                button1.IsEnabled = true;

                return;

            }

            WebClient client = new WebClient();

            client.OpenReadCompleted += (s, args) =>

                {

                    if (args.Error != null)

                    {

                        MessageBox.Show(args.Error.Message);

                        button1.IsEnabled = true;

                        return;

                    }

                    Image img = new Image();

                    BitmapImage bmp = new BitmapImage();

                    bmp.SetSource(args.Result);

                    img.Source = bmp;

                    picPanel.Children.Add(img);

                    LoadImage(list, ++index);

                };

            client.OpenReadAsync(new Uri("../" + list[index], UriKind.Relative));

}

 

private   void button1_Click(object sender, RoutedEventArgs e)

{

            button1.IsEnabled = false;

             WebClient client = new WebClient();

             client.DownloadStringCompleted += (s, args) =>

                 {

                     if (args.Error != null)

                     {

                         MessageBox.Show(args.Error.Message);

                         return;

                     }

 

                     List<string> list = new List<string>();

                     using (StringReader sr = new StringReader(args.Result))

                     {

                         while (sr.Peek() != -1)

                             list.Add(sr.ReadLine());

                     }

 

                     LoadImage(list, 0);

                 };

 

             client.DownloadStringAsync(new Uri("../pics.txt", UriKind.Relative));

}

 

Silverlight的非同步網路技術,讓我們可以在不停滯UI的情況下,進行網路動作,但同時也給予了我們很大的挑戰,以同步寫法來說,根本就

不需要寫遞迴就能達成同樣的效果。這點在WCF Service呼叫及ChildWindow上都表露無遺,成為很多設計師進入Silverlight領域時最大的阻礙。

 

 

 

 Boom!! Visual Studio Async CTP 

 

 

  在PDC 10中,Anders發表了一個跨時代的技術: Visual Studio Async CTP,在安裝後,上例可以改寫為下面這樣子。

 

MainPage.xaml.cs

using   System;

using   System.Collections.Generic;

using   System.Linq;

using   System.Net;

using   System.Windows;

using   System.Windows.Controls;

using   System.Windows.Documents;

using   System.Windows.Input;

using   System.Windows.Media;

using   System.Windows.Media.Animation;

using   System.Windows.Shapes;

using   System.Windows.Media.Imaging;

using   System.IO;

 

namespace   AsyncPictureShow

{

    public partial class MainPage : UserControl

    {

        public MainPage()

        {

            InitializeComponent();

        }

 

        private async void button1_Click(object sender, RoutedEventArgs e)

        {

            button1.IsEnabled = false;

            WebClient client = new WebClient();

            try

            {

                string result = await client.DownloadStringTaskAsync(

                                           newUri("../pics.txt", UriKind.Relative));

 

                using (StringReader sr = new StringReader(result))

                {

                    while (sr.Peek() != -1)

                    {

                        string imgFile = sr.ReadLine();

                        Image img = new Image();

                        BitmapImage bmp = new BitmapImage();

                        bmp.SetSource(await client.OpenReadTaskAsync(

                                   new Uri("../" + imgFile, UriKind.Relative)));

                        img.Source = bmp;

                        picPanel.Children.Add(img);

                    }

                }

            }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

            button1.IsEnabled = true;

        }

    }

}

 

執行結果與上完全相同,但寫法上卻很像是同步寫法,這就是Visual Studio Async CTP的威力,將非同步寫法呈現出同步寫法的外觀,比起上例使用Lambda Expression及

遞迴的寫法來說,這個嶄新的寫法不僅直覺且簡單很多。

 

  Visaul Studio Async CTP目前可於以下網址取得。

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=18712f38-fcd2-4e9f-9028-8373dc5732b2

 

提醒各位,Visual Studio Async CTP目前僅支援安裝於Visual Studio 2010英文版上,當安裝後,你便可以在.NET Framework 4 的專案中添加AsyncCtpLibrary.dll(於MyDocument\Visual Studio Async CTP\Samples目錄)

為Reference即可使用await及async關鍵字。

在Silverlight專案中,則是添加AsyncCtpLibrary_Silverlight.dll(於MyDocument\Visual Studio Async CTP\Samples目錄)為Reference。

 

 

發生什麼事了??

 

    當將某一個函式以async宣告時,在該函式中便可使用await關鍵字,其右方必須是一個可回傳Task<T>的函式,當編譯器開始編譯這段程式碼時,會將其展開,await會被展開成一段迴圈,

一直等待到Task<T>的回傳值被設定才會結束迴圈,因此會造成一個假象,好像是await呼叫完成並取得結果後,才會執行下一行程式碼。當然,這是我簡化後的流程,事實上編譯器展開await的過程複雜多了。

   至於DownloadStringAsyncTask及OpenReadAsyncTask兩個函式,則是針對WebClient設計的Extension Method(擴充函式),因為原先的DownloadStringAsync、OpenReadAsync回傳值

不是Task<T>,所以不能直接用在await右方。 

PS:關於Task<T>的作用,可參考我的Parallel Programming系列文章。

 

 

可以用在WCF Service上嗎?

 

     現在我們知道,await可以用在能回傳Task<T>的函示呼叫上,很幸運的,Visual Studio Async CTP提供了WebClient專用的DownloadAsyncTask等函式供我們使用,

但如果是我們自定義的WCF Service呢?

    目前Visual Studio Async CTP尚未提供對應的Extension Method,不過我們可以自己做,只要寫一小段Wrapper程式即可。

 

IService.cs

     [ServiceContract]

    public interface IService1

    {

        [OperationContract]

        string[] GetPicList();

 

        [OperationContract]

        byte[] GetPic(string picName);

    }

MainPage.xaml.cs

using  System;

using   System.Collections.Generic;

using   System.Collections.ObjectModel;

using   System.Linq;

using   System.Net;

using   System.Windows;

using   System.Windows.Controls;

using   System.Windows.Documents;

using   System.Windows.Input;

using   System.Windows.Media;

using   System.Windows.Media.Animation;

using   System.Windows.Shapes;

using   System.Windows.Media.Imaging;

using   System.Threading;

using   System.Threading.Tasks;

using   System.IO;

 

namespace  PicShow_UseWcf

{

      public partial class MainPage : UserControl

      {

        public MainPage()

        {

            InitializeComponent();

        }

 

        private Task<ObservableCollection<string>> LoadPicList()

        {

            ServiceReference1.Service1Client client =

                           newServiceReference1.Service1Client();

            TaskCompletionSource<ObservableCollection<string>> tcs =

                           new TaskCompletionSource<ObservableCollection<string>>();

            client.GetPicListCompleted += (s, args) =>

                {

                    tcs.TrySetResult(args.Result);

                };

            client.GetPicListAsync();

            return tcs.Task;

        }

 

        private Task<byte[]> LoadPic(string name)

        {

            ServiceReference1.Service1Client client =

                                       newServiceReference1.Service1Client();

            TaskCompletionSource<byte[]> tcs = new TaskCompletionSource<byte[]>();

            client.GetPicCompleted += (s, args) =>

            {

                tcs.TrySetResult(args.Result);

            };

            client.GetPicAsync(name);

            return tcs.Task;

        }

 

        async void LoadImages()

        {

            button1.IsEnabled = false;

            try

            {

                var list = await LoadPicList();

                foreach (var item in list)

                {

                    Image img = new Image();

                    BitmapImage bmp = new BitmapImage();

                    byte[] buff = await LoadPic(item);

                    MemoryStream ms = new MemoryStream(buff);

                    bmp.SetSource(ms);

                    img.Source = bmp;

                    picPanel.Children.Add(img);

                }

 

            }

            catch(Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

            finally

            {

                button1.IsEnabled = true;

            }

        }

 

        private void button1_Click(object sender, RoutedEventArgs e)

        {

            LoadImages();

        }

    }

}

 

請特別注意LoadPicList與LoadPic,這兩個函式所回傳的型別是Task<T>,這意味著其可以用在await的右方,當tcs.TrySetResult被執行時,

也意味著await的動作會結束等待,程式將會往下一行走,另一點需注意的是,使用await的函式必須是宣告為async。

同樣的手法也能用在WCF Data Service及WCF RIA Services上。

 

 

關於ChildWindow

 

   除了WCF Service等呼叫的動作外,同樣也困擾著Silverlight使用者的還有ChildWindow,以往在巢狀ChildWindow呼叫時,我們都得使用Lambda Expression一層一層的做,

有了Visual Studio Async後,一切都變得簡單了。

 

private Task<bool> ShowDialog(ChildWindow cw)

{

            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

            cw.Closed += (s, args) =>

                {

                    tcs.TrySetResult(cw.DialogResult.Value);

                };

            cw.Show();

            return tcs.Task;

}

 

async void button1_Click(object sender, RoutedEventArgs e)

{

            SelectCity cw1 = new SelectCity();

            bool selCity = await ShowDialog(cw1);

            if (selCity)

            {

                SelectArea cw2 = new SelectArea();

                if (((ComboBoxItem)cw1.comboBox1.SelectedValue).Content.Equals(

                                            "台北市"))

                {

                    cw2.comboBox1.Items.Add("文山區");

                    cw2.comboBox1.Items.Add("大安區");

                }

                else if (((ComboBoxItem)cw1.comboBox1.SelectedValue).Content.Equals(

                                           "台北縣"))

                {

                    cw2.comboBox1.Items.Add("永和");

                    cw2.comboBox1.Items.Add("中和");

                }

                bool selArea = await ShowDialog(cw2);

                if (selArea)

                    textBlock1.Text =

                                            ((ComboBoxItem)cw1.comboBox1.SelectedValue).Content.ToString() +

                                      cw2.comboBox1.SelectedValue.ToString();

            }

}

 

一切都是編譯器的戲法 !!!

 

     雖然目前Visual Studio Async CTP只能安裝於英文的Visual Studio 2010上,但其編譯出來的程式可以執行在已安裝.NET Framework 4.0 及Silverlight 4的機器上,該機器不需要

安裝Visual Studio Async CTP,簡單的說!這一切都是編譯器展開後的結果,其編譯出來的程式仍然是標準的CLR Code。

   

 

Complier as Service! 

 

 

  在末尾,Anders 還展示了一個新技術,未來的編譯器將朝著Complier as Service方向走,在不久的將來,我們將可以以程式透過Complier Service來擴展編譯器及IDE的功能,Anders

於此場中展示了一個範例,直接將C#透過這個機制完整轉成VB.NET,而且只要寫少少的程式碼就可以完成。