來玩Silverlight 3吧 (1) - DataForm

如許多人所預期,Silverlight 3的第一個測試版在今年MIX 09中現身,就架構而言,Silverlight 3其實就如同ASP.NET 2.0與3.5間的關係,僅是立基於原本的Silverlight 2上持續演化而已。

 

來玩Silverlight 3吧 (1) - DataForm
 
/黃忠成
 
本文術語註記

 

Entity Class
 當你使用LINQ To SQL或是ADO.NET Entity Framework來產生O/R Mapping類別時,Entity Class指的就是資料列所對應的類別,舉例來說,當產生Customers資料表對應的O/R Mapping類別時,那麼Customers類別就是Entity Class,每一個Entity Class所產生出來的實體就叫Entity Object。
ADO.NET Data Service Client Proxy Class
這個名詞與Web Services Proxy Class類似,以前當我們要以C#VB.NET呼叫Web Services時,通常會用Add Web References來新增一個Web 參考,這個動作會產生一個類別,這個類別會將實際的網路通訊程式碼封裝起來,便於使用。因此,透過這個類別,我們可以直接以函式呼叫的方式來呼叫Web Services,而不需理會內部的 SOAP訊息組成細節。
   ADO.NET Data Services Client Proxy Class顧名思意就是由Visual Studio 2008產生的ADO.NET Data Services參考,此類別中提供了一些簡單易懂的函式供我們呼叫,我們無需理會執行這些函式時會送出什麼樣的HTTP Request
 
What's New In Silverlight 3
 
    如許多人所預期,Silverlight 3的第一個測試版在今年MIX 09中現身,就架構而言,Silverlight 3其實就如同ASP.NET 2.03.5間的關係,僅是立基於原本的Silverlight 2上持續演化而已。在Silverlight 3中,除了支援更高解析度的影片外,還預期會支援3D,這是許多RIA架構未來都會走的方向。撇開這些顯而易見的亮點,Silverlight 3也添加了許多豐富的控件,其中許多控件是來自Silverlight Toolkit。對我而言,Silverlight 3中添加多麼炫麗的控件都引不起我的興趣,因為SilverlightControls Model先天上的延展性設計,要做出很棒、很炫的控件都不是件難事,難的是在創意而已。但我對Silverlight 3的資料模型很感興趣,前陣子曾與同好討論過,Silverlight 2的資料模型對初學者仍然稍嫌複雜,因為你必須建個ADO.NET Data Service,然後用Silverlight 2來編修資料,建個ADO.NET Data Service並不難,用Silverlight 2來擷取資料後繫結至DataGrid也不難,難的是,當你開始在DataGrid上修改資料並嘗試寫入資料庫時,問題就會一一浮現。
 
    以目前官方的例子來說,設計師必須處理DataGrid上編修資料的事件,然後用程式將資料寫回後端。會發生這種需程式設計師間接介入的原因很簡單,預設的Silverlight Tools for Visual Studio 2008所產生的ADO.NET Data Service Client Proxy Class內的Entity Class並未支援IEditableObject介面,但是DataGrid等資料控件卻仍然依據此介面而設計。要了解IEditableObject介面的角色,讀者可以參考一些Windows Forms的程式設計書籍。這點,在目前的Sivlerlight 3中依然如此,在Silverlight Team找出比產生Support IEditableObject 介面的Entity Class之前,設計師的介入是絕對必要的。既然資料模型尚未做出改變,那麼現在我們就只能夠針對與其息息相關的資料控件了,除了原來的多筆瀏覽用DataGrid控件外,Silverlight 3中還添加了一個單筆資料編修控件DataForm,這個控件如同ASP.NET FormView一般,可讓使用者一筆筆的瀏覽及編修資料,這比起以前要自己手動放TextBox並設定Binding DataContext方便了許多。
 
準備你的環境
 
    要體驗Silverlight 3中的DataForm控件,當然得先安裝好Silverlight 3相關的Package,以下是這些Package列表及下載網址:
  • Silverlight 3 Tools Beta 1 for Visual Studio 2008
  • Expression Blend 3 Preview
       當然,如果只是要執行Silverlight 3的客戶端,只需下載Silverlight 3 Runtime即可。
  • Silverlight 3 Runtime
 
 
DataForm控件
 
       DataForm控件與ASP.NET FormView控件相似,都是提供單筆資料編輯、導覽的功能,圖1是其執行時期畫面
1
基於Silverlight 控件的延展性,DataForm的所有欄位及按紐樣式當然都是可自訂的,也就是說你可以使用DatePicker來編修日期,使用ComboBox來讓使用者選擇預先定義的資料項,或是換掉上面導覽列的按紐,這些都不難達成。不過本文的目的不在於教導如何自訂這些區塊的樣式,而是著眼於最基本的資料導覽及編修上。
 
       與所有的資料控件相同,DataForm提供了一個名為ItemsSource的屬性,我們可以將一個實作IEnumerable介面的物件設給此屬性,之後DataForm就會逐筆的顯示該物件集中的元素。
 那這個實作IEnumerable介面的物件從何而來呢?就實務上而言,這個物件通常是呼叫ADO.NET Data Services所得到的,因為DataForm的目的就是要讓使用者可以逐筆編修資料,所以這些資料自然是儲存於後端的資料庫中了。
 
準備ADO.NET Data Services
 
      從ASP.NET 3.5開始,ADO.NET Data Services就成了Silverlight應用程式最佳的資料來源,於Silverlight 2.0時,ADO.NET Data Services只支援ADO.NET Entity Framework,但在Silverlight 3未來的藍圖中,LINQ To SQL也在支援之列,不過目前所取得的3.0測試版中仍然只支援ADO.NET Entity Framework。
      因此,要撰寫ADO.NET Data Services,我們得先建立一個ADO.NET Entity Framework Model,請開啟Visual Studio 2008,然後選擇建立一個Silverlight應用程式。
圖2
完成後會得到兩個專案,一是Silverlight Application,另一個是Silverlight Application的ASP.NET Web Site或是Web Application專案。
於ASP.NET Web Site或Web Application專案中新增一個ADO.NET Entity Data Model。
圖3
選擇由Northwind資料庫匯入。
4
匯入Customers資料表至此Model中。
5
按下Finish按紐後便完成了ADO.NET Entity Data Model的建立動作。接著我們在ASP.NET Web Site或Web Application中持續加入一個新專案項目:ADO.NET Data Services。
圖6
修改產生出來的程式碼,在Generic Type Argument(泛型型別)處放上Entity Data Model的類別。
程式1
WebDataService1.svc.vb
Imports System.Data.Services
Imports System.Linq
Imports System.ServiceModel.Web
 
Public Class WebDataService1
    ' TODO: replace [[class name]] with your data class name
    Inherits DataService(Of NorthwindEntities)
    ' This method is called only once to initialize service-wide policies.
    Public Shared Sub InitializeService(ByVal config As IDataServiceConfiguration)
        ' TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
        ' Examples:
        config.SetEntitySetAccessRule("*", _
               EntitySetRights.AllRead Or EntitySetRights.AllWrite)
   End Sub
End Class
以IE確認Service正常運作。
圖7
 
 
DataForm結合
 
    接下來就是用Silverlight 3透過此Data Service來擷取資料,先以Add Service Reference來取得ADO.NET  Data Services Client Proxy
8
9
10
    按下OK按紐後,Visual Studio 2008即會產生ADO.NET Data Services的Proxy Class,這個Proxy Class封裝了所有的HTTP Request,可讓我們用少許的程式碼來操控後端的資料庫。
最後在MainPage.xaml中鍵入程式2的程式碼。
程式2
MainPage.xaml
<UserControl xmlns:dataControls=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm" 
    x:Class="SilverlightApplication1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="400" Height="500" Loaded="UserControl_Loaded">
    <Grid x:Name="LayoutRoot" Background="White">
      <dataControls:DataForm x:Name="form1" AutoGenerateFields="True">
     </dataControls:DataForm>
    </Grid>
</UserControl>
在MainPage.xaml.vb中鍵入程式3的程式碼。
程式3
MainPage.xaml.vb
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Data.Services.Client
 
Partial Public Class MainPage
    Inherits UserControl
 
    Private _ctx As ServiceReference1.NorthwindEntities
    Private _data As ObservableCollection(Of ServiceReference1.Customers)
 
    Public Sub New()
        InitializeComponent()
    End Sub
 
    Private Sub UserControl_Loaded(ByVal sender As System.Object, _
                 ByVal e As System.Windows.RoutedEventArgs)
        Dim service As New ServiceReference1.NorthwindEntities( _
                   New Uri("WebDataService1.svc", UriKind.Relative))
        service.BeginExecute(Of ServiceReference1.Customers)( _
           New Uri("Customers", UriKind.Relative), _
           New AsyncCallback(AddressOf MyLoadBack), service)
    End Sub
 
    Sub MyLoadBack(ByVal state As IAsyncResult)
        _ctx = CType(state.AsyncState, ServiceReference1.NorthwindEntities)
        _data = New ObservableCollection(Of ServiceReference1.Customers)()
        For Each item In _ctx.EndExecute(Of ServiceReference1.Customers)(state)
            _data.Add(item)
        Next
        form1.ItemsSource = _data
    End Sub
End Class
完成後執行程式,即可看到圖11的執行畫面。
11
看起來DataForm已經幫我們產生了一個可瀏覽及編修的頁面是吧?不過別高興太早,實際上這個頁面只有讀取後端資料庫的能力而已,其它的編輯、新增、刪除等功能都得靠我們寫程式解決。
 
 
Silverlight 3 Data Model
 
     在Silverlight 2/3中,Data Model其實就是ADO.NET Data Services,Silverlight 2/3中包含了一組可與ADO.NET Data Services溝通的Client Library。以取得資料而言,由Visual Studio 2008所產生的ADO.NET Data Services Client Proxy Class中,會透過這個Client Library以HTTP Request方式來呼叫ADO.NET Data Services擷取資料,程式3的UserControl_Loaded函式中做的就是這件事。
程式4
Dim service As New ServiceReference1.NorthwindEntities( _
                   New Uri("WebDataService1.svc", UriKind.Relative))
 service.BeginExecute(Of ServiceReference1.Customers)( _
              New Uri("Customers", UriKind.Relative), _
             New AsyncCallback(AddressOf MyLoadBack), service)
BeginExecute函式需要填入一個欲取得資料表的對應Entity  Class,以本例而言就是Customers,第一個參數則是需填入資料表的名稱,注意!這是以相對URL方式傳入的,最後一個參數則是取得資料後,回呼的Callback函式
 
    先天上,Silverlight 2/3所有的網路動作都採非同步方式呼叫,也就是說許多的網路函式都是BeginXXX、EndXXX成對出現,以BeginExecute函式為例,BeginExecute意指以非同步方式送出一個HTTP Request(你可以想像成,BeginExecute建立了一個Thread,然後把送出HTTP Request的程式寫在Thread中執行),當呼叫完畢後,不管成功或是失敗,都會回呼你所指定的函式,也就是MyLoadBack。
 
    當回呼回MyLoadBack後,此處就以EndExecute來取得函式的回傳值,以本例而言,取得的回傳值是一個包含Customers資料表中所有資料列的Enumerable物件。
程式5
Sub MyLoadBack(ByVal state As IAsyncResult)
        _ctx = CType(state.AsyncState, ServiceReference1.NorthwindEntities)
        _data = New ObservableCollection(Of ServiceReference1.Customers)()
        For Each item In _ctx.EndExecute(Of ServiceReference1.Customers)(state)
            _data.Add(item)
        Next
        form1.ItemsSource = _data
End Sub
之後,我們建立一個ObservableCollection物件,一一將透過EndExecute取得的Enumerable內元素填入。使用ObservableCollection的理由很簡單,ObservableCollection支援Item Add/Delete的Tracking機制。簡單的說,透過EndExecute取得的Enumerable物件僅提供列舉能力,當需要新增/刪除資料時,就得透過另一個支援新增、刪除的Collection物件進行,這通常是List,不過List並不支援新增Item、刪除Item時的Tracking動作,也就是說當使用List時,如果設計師透過Add函式新增一個元素,那麼我們將無從得知此新增動作,當使用ObservableCollection時,Add函式則會觸發一個CollectionChanged事件,我們可以藉由掛載此事件來進行元素新增的後續動作。
 
     這種模式其實是一種Design Patterns,一般來說,資料載體應該是獨立的,所有新增/刪除元素的動作都該在此資料載體中完成,不需要設計師撰寫多餘的程式碼,也就是說,你透過Add函式來新增一個元素,但你不用擔心元素新增後,該如何寫入資料庫中,後續動作均由資料載體完成。
 話雖如此,不過由於Silverlight 2/3的寫入資料庫是透過HTTP Request完成的,如果每一個Add函式都觸發一次HTTP Request,那麼應用程式效能將會變的很低,所以Silverlight 2/3採用了Batch Request模式,不在資料載體中自行處理,而是要求設計師明確呼叫BeginSaveChanges函式。當BeginSaveChanges函式開始執行時,資料載體將會搜集截至目前為止的變動元素,以單一HTTP Request方式更新後端資料庫。
 
       看到這裡,你大概得出了一個結論,那就是這裡其實不一定要用ObservableCollection,尤其是在使用DataForm的情況,因為不管如何,我們都不會掛載事件到ObservableCollection來處理新增/刪除的後續資料庫寫入動作,因為這有效率低落的問題,所以接下來的新增、修改、刪除功能,一律透過DataForm所提供的事件處理。
 
完成Update功能
 
   當使用者在DataForm上按下編修資料按紐時,該筆資料會變成可編修,下方會出現一個Save按紐,在使用者修改資料後並按下Save按紐後,DataForm的ItemEditEnded事件就會觸發,在此我們以BeginSaveChanges來將修改後的資料寫回資料庫。
程式6
Private Sub form1_ItemEditEnded(ByVal sender As System.Object, _
          ByVal e As System.Windows.Controls.DataFormItemEditEndedEventArgs)
        If e.EditAction = DataFormEditAction.Commit Then
            form1.CommitItemEdit()
            _ctx.UpdateObject(form1.CurrentItem)
            _ctx.BeginSaveChanges( _
                  New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
        End If
 End Sub
    這裡有兩個關鍵函式,一是UpdateObject函式,此函式會將傳入的Entity Object狀態設成已修改,在後續的BeginSaveChanges函式執行時,會依據Entity Object的狀態來決定送出那些資料列。
 當BeginSaveChanges執行完畢後,會回呼我們所設定的SaveComplete函式,下面是此函式的原始碼。
程式7
Sub SaveComplete(ByVal state As IAsyncResult)
        Try
            _ctx.EndSaveChanges(state)
        Catch ex As DataServiceRequestException
            MessageBox.Show(ex.Message)
        End Try
        MessageBox.Show("Save Complete")
 End Sub
請注意,不管BeginSaveChanges是否成功的呼叫ADO.NET Data Services並將變動資料寫入資料庫,SaveComplete函式一定會被呼叫,如果寫入或呼叫ADO.NET Data Services的動作有錯誤,那麼呼叫EndSaveChanges函式時,就會產生例外,於此可補捉錯誤的詳細訊息。
 
完成Delete功能
 
    相對於修改功能,Delete功能簡單的多了,當使用者按下DataForm上的刪除按紐時,DeleteItem事件會被觸發,於此我們可以呼叫ADO.NET Data Services來刪除後端的資料列。
程式8
Private Sub form1_DeletingItem(ByVal sender As System.Object, _
           ByVal e As System.ComponentModel.CancelEventArgs)
        _ctx.DeleteObject(form1.CurrentItem)
        _ctx.BeginSaveChanges(New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
 End Sub
UpdateObject函式類似,DeleteObject函式會將傳入的Entity Object狀態設為已刪除,當BeginSaveChanges執行時,此Entity Object就會被送往後端刪除。
 
完成Insert 功能
 
       最後完成的是Insert功能,與修改及Delete功能不同,當使用者按下DataForm上的新增按紐時,只有一個AddingItem事件會被觸發,而後便進入與修改資料列一樣的流程,當使用者按下Save按紐時,ItemEditEnded事件就會被觸發,這意味著我們得在ItemEditEnded事件中一起處理InsertUpdate動作。
程式9
Private Sub form1_ItemEditEnded(ByVal sender As System.Object, _
              ByVal e As System.Windows.Controls.DataFormItemEditEndedEventArgs)
        If e.EditAction = DataFormEditAction.Commit Then
            If _inAppending Then
                Try
                    _ctx.AddToCustomers(form1.CurrentItem)
                Catch ex As Exception
                    ' the item may be added.
                End Try
                _inAppendingIndex = form1.CurrentIndex
                _ctx.BeginSaveChanges( _
               New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
            Else
                _ctx.UpdateObject(form1.CurrentItem)
                _ctx.BeginSaveChanges(_
                  New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
            End If
    End Sub
 
    Private Sub form1_AddingItem(ByVal sender As System.Object, _
            ByVal e As System.ComponentModel.CancelEventArgs)
        _inAppending = True
    End Sub
於此,我們用了一個_inAppending旗標來識別目前編修的資料列是修改還是新增,設定此旗標的最佳位置自然是使用者按下新增按紐後所觸發的AddingItem事件 。當判定為新增時,我們就不呼叫UpdateObject函式,而是呼叫AddToCustomers函式來將此筆資料標示為新增,稍後BeginSaveChanges就會將此筆資料以新增的狀態送往後端。
     AddToCustomers是由Visual Studio 2008所自動產生的函式,格式為AddTo<資料表名稱>,也就是說當資料表名稱為Employees時,就會有一個AddToEmployees函式產生。程式10是完整的MainPage.xaml程式碼。
程式10
MainPage.xaml
<UserControl xmlns:dataControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm" 
    x:Class="SilverlightApplication1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="400" Height="500" Loaded="UserControl_Loaded">
    <Grid x:Name="LayoutRoot" Background="White">
      <dataControls:DataForm x:Name="form1" AutoGenerateFields="True"  ItemEditEnded="form1_ItemEditEnded"  AutoCommit="False"
                             AddingItem="form1_AddingItem" DeletingItem="form1_DeletingItem"  ></dataControls:DataForm>
    </Grid>
</UserControl>
 
MainPage.xaml.vb
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Data.Services.Client
 
Partial Public Class MainPage
    Inherits UserControl
 
    Private _ctx As ServiceReference1.NorthwindEntities
    Private _data As ObservableCollection(Of ServiceReference1.Customers)
    Private _inAppending As Boolean = False
    Private _inAppendingIndex As Integer = 0
 
    Public Sub New()
        InitializeComponent()
    End Sub
 
    Private Sub UserControl_Loaded(ByVal sender As System.Object, _
            ByVal e As System.Windows.RoutedEventArgs)
        Dim service As New ServiceReference1.NorthwindEntities( _
                  New Uri("WebDataService1.svc", UriKind.Relative))
        service.BeginExecute(Of ServiceReference1.Customers)( _
              New Uri("Customers", UriKind.Relative), _
              New AsyncCallback(AddressOf MyLoadBack), service)
    End Sub
 
    Sub MyLoadBack(ByVal state As IAsyncResult)
        _ctx = CType(state.AsyncState, ServiceReference1.NorthwindEntities)
        _data = New ObservableCollection(Of ServiceReference1.Customers)()
        For Each item In _ctx.EndExecute(Of ServiceReference1.Customers)(state)
            _data.Add(item)
        Next
        form1.ItemsSource = _data
    End Sub
 
    Sub SaveComplete(ByVal state As IAsyncResult)
        Try
            _ctx.EndSaveChanges(state)
        Catch ex As DataServiceRequestException
            MessageBox.Show(ex.Message)
            form1.CurrentIndex = _inAppendingIndex
            form1.BeginEdit()
            Return
        End Try
        MessageBox.Show("Save Complete")
        _inAppending = False
    End Sub
 
    Private Sub form1_ItemEditEnded(ByVal sender As System.Object, _
              ByVal e As System.Windows.Controls.DataFormItemEditEndedEventArgs)
        If e.EditAction = DataFormEditAction.Commit Then
            If _inAppending Then
                Try
                    _ctx.AddToCustomers(form1.CurrentItem)
                Catch ex As Exception
                    ' the item may be added.
                End Try
                _inAppendingIndex = form1.CurrentIndex
                _ctx.BeginSaveChanges( _
                     New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
            Else
                _ctx.UpdateObject(form1.CurrentItem)
                _ctx.BeginSaveChanges( _
                     New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
            End If
        Else
            form1.CancelItemEdit()
        End If
    End Sub
 
    Private Sub form1_AddingItem(ByVal sender As System.Object, _
                  ByVal e As System.ComponentModel.CancelEventArgs)
        _inAppending = True
    End Sub
 
    Private Sub form1_DeletingItem(ByVal sender As System.Object, _
                 ByVal e As System.ComponentModel.CancelEventArgs)
        If _inAppending Then
            _inAppending = False
            _ctx.Detach(form1.CurrentItem)
            _data.Remove(form1.CurrentItem)
            If form1.CurrentIndex > 0 Then
                form1.CurrentIndex = form1.CurrentIndex - 1
            End If
            Return
        ElseIf form1.CanCancelEdit Then
            form1.CancelItemEdit()
        End If
        _ctx.DeleteObject(form1.CurrentItem)
        _ctx.BeginSaveChanges(New AsyncCallback(AddressOf SaveComplete), form1.CurrentItem)
    End Sub
End Class
 
 
完整嗎? may be not !
 
   雖然看起來,DataForm似乎扮演著很好的單筆資料編修控件角色,但事實不然,如果你仔細把玩後,你會發現DataForm無法處理兩件事,當修改資料後,想放棄修改回復原值時,似乎沒有適當的事件可以處理。相同的情況也出現在意圖放棄新增動作時,這兩個問題都與一個介面有關:IEditableObject。有趣的是,DataForm及DataGrid等資料控件內部都有針對已實作IEdtiableObject介面的物件進行適當的處理,但Visual Studio 2008所產生的ADO.NET Data Services Client Proxy卻未將Entity Class產生為實作IEditableObject介面的類別,導致使用DataGrid、DataForm做為UI時,設計師得花上很多時間來實作取消的功能。
 
Next Time - Lets' Implement IEditableObject
 
    在下一篇文章中,我將提供一個簡單的Plug-In Tools,協助產生實作IEditableObject介面的Entity Class。