在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,而且只要寫少少的程式碼就可以完成。