Silverlight 2.0 資料庫應用程式開發(1)

我想不用我贅言,相較於Silverlight 1.0的陽春,Silverlight 2.0提供了更完整的資料庫支援,其最主要的部份在於提供了WCF/HTTP的網路機制及客戶端的控件Binding技術,有了這兩個機制,我們可以透過WCF/HTTP網路機制連回Server端取得資料,然後以Binding技術將控件與資料結合在一起,設計出類似Windows Form/WPF UI介面的強大網頁資料庫應用程式

 

Silverlight 2.0 資料庫應用程式開發(1)
 
 
/黃忠成
 
Silverlight 1.0 Silverlight 2.0
 
   我想不用我贅言,相較於Silverlight 1.0的陽春,Silverlight 2.0提供了更完整的資料庫支援,其最主要的部份在於提供了WCF/HTTP的網路機制及客戶端的控件Binding技術,有了這兩個機制,我們可以透過WCF/HTTP網路機制連回Server端取得資料,然後以Binding技術將控件與資料結合在一起,設計出類似Windows Form/WPF UI介面的強大網頁資料庫應用程式。
 
Silverlight 2.0 N-Tier
 
   對於部份的ASP.NET設計師而言,對Silverlight 2.0的資料庫應用程式的開發模式會有些陌生,在ASP.NET設計模式中,我們可以直接使用ADO.NET來取得資料,接著利用GridView等資料控件將資料顯現於網頁上,此時的系統架構如圖SL001
SL001
但在Silverlight 2.0中,並未提供ADO.NET這個機制,從網頁應用程式角度上看,Silverlight 2.0是一種Client Application架構,運行於IE等瀏覽器平台之上,因此若要提供ADO.NET機制,那麼後端的資料庫必定得曝露於網際網路之上,而你肯定無法接受將SQL Server曝露在網路上供人存取,因為這會帶來安全性、授權費等問題。所以,當使用Silverlight 2.0時,我們必須找一個相對於ADO.NET的機制來做為取得資料的中介層,這種設計模式就稱為N-Tier,見圖SL002
SL002
如圖SL002所示,Silverlight應用程式是內嵌於HTML中下載,她可以透過WCF/HttpWebRequest網路機制連回來源的Web Site取得資料,問題在於後端的Web Site如何接收來自Silverlight的網路要求?在Silverlight 2.0中,你有兩種選擇,一是在Web Site端建立一個簡單的網頁,收取來自Silverlight的網路要求,接著輸出XML或是JSON格式的資料。二是透過.NET WCF機制,於Web Site端建立一個WCF Service,於Silverlight中透過此WCF Service來取得資料。兩種機制各有其優缺點,方法一可以適用於所有網頁平台,例如ASPPHPJSP皆可,而方法二則是僅限於支援Web Service的網頁平台。
 
 在開始設計實際應用程式之時,你必須先了解所謂N-Tier架構的定義,所謂的N-Tier指的是將整個程式架構切分為三個層面,第一層是展現層,也就是你的Silverlight應用程式,用來顯示資料及接受使用者操作,第二層指的服務層,用來提供資料及回應來自展示層的要求,第三層則是資料層,用來由資料庫取得資料或是更新資料。N-Tier早期的發展是為了分擔資料庫系統的繁重工作,以[偶而連線][負載平衡]等技巧,減輕本來必須全部交與資料庫處理的工作。舉個例來說,一系統擁有10000個使用者,當使用傳統的Client/Server架構時,這10000個使用者會握有10000個資料連線,當這些使用者發出要求資料的SELECT需求時,資料庫系統就得負擔產生10000個資料集的工作,但這10000個需求中,很有可能有一半以上是要求同一份資料。當使用N-Tier架構時,我們就可以在服務層中快取已取過的資料,當有同樣要求來臨時,直接以回傳快取資料的方式取代由資料庫系統取得的動作,這樣自然就減輕了資料庫的負擔,提高系統所能承載的用戶數量。
 
 在早期網際網路頻寬不足的環境下,N-Tier架構原始設計中每個動作都要透過連結服務層的模式顯得有些不切實際,因為使用者可能處於一個低頻寬的環境、甚至是無網路可用的窘境,在這種模式下,存取服務層是種奢侈。因此,N-Tier發展出一種名為[偶爾連線]的模式,展示層在有網路的情況下,將資料由服務層取回後快取於客戶端,此時使用者可以自由的操作展示層來新增、修改、刪除及查詢資料,這些動作全以客戶端的快取為主,待有網路時,再將異動資料整批傳回服務層更新回資料庫。
 
 [偶爾連線]的架構很完美,也很符合當年的需求,但隨著網路的普遍化及高覆蓋率,設計[偶而連線]架構所需付出的代價就顯得有些累贅了,[偶爾連線]的架構建立在完善的客戶端快取機制及客戶端查詢機制下,而這些機制並不容易建構。所以,現在的N-Tier轉變為以[連線模式]為主。
 
 Silverlight 2.0的架構趨近於[連線模式],這意味著Silverlight 2.0的應用程式將運行在一個可存取網站的環境下,當這個網站存在於本機時,程式不需要網路便可執行,當網站不存在於本機時,則需要網路方能運行。

 

註:多數瀏覽器都有一種以離線模式執行網頁的設定,將離線模式打開,你依舊能在離線模式下運行Silverlight程式,在適當的設計下,其實也能做出[偶爾連線]模式。
 
 
使用HttpWebRequest
 
 在大略了解Silverlight 2.0N-Tier架構後,我們便可以開始撰寫Silverlight 2.0的資料庫應用程式,請建立一個Silverlight 2.0專案,接著於Web結尾之Project添加一資料庫。
SL003
SL004
然後添加一資料表至資料庫中。
SL005
建立結構。
SL006
添加資料列。
SL007
SL008
完成資料庫準備工作後,現在便可開始實作服務層,在這小節中,我們以傳統網頁模式來建立服務層,請於Web結尾之專案中添加一Generic Handler(你也可以使用.aspx)
SL009

 

DBProvider.ashx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Xml.Linq;
 
namespace DBDemo1.Web
{
    ///<summary>
    /// Summary description for $codebehindclassname$
    ///</summary>
    public class DBProvider : IHttpHandler
    {
 
        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/xml";
            using (SqlConnection conn = new SqlConnection(@"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\MyDatabase.mdf;Integrated Security=True;User Instance=True"))
            {
                XDocument doc = new XDocument(new XElement("Root"));
                using (SqlCommand cmd = new SqlCommand("SELECT * FROM CUSTOMERS", conn))
                {
                    conn.Open();
                    using (SqlDataReader reader = cmd.ExecuteReader(
                                         CommandBehavior.CloseConnection))
                    {
                        while (reader.Read())
                        {
                            XElement elem = new XElement("Customer");
                            elem.Add(new XAttribute("CUSTOMER_ID",
                               reader.GetString(reader.GetOrdinal("CUSTOMER_ID"))));
                            elem.Add(new XAttribute("CUSTOMER_NAME",
                               reader.GetString(reader.GetOrdinal("CUSTOMER_NAME"))));
                            doc.Root.Add(elem);
                        }
                    }
                    context.Response.Write(doc.ToString());
                    context.Response.Flush();
                    context.Response.End();
                }
            }
        }
 
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
透過IE來測試此服務層是否運作正常。
SL010
由於本例將使用DataGrid控件,因此你必須透過Add Reference(加入參考)來添加DataGrid所需要的AssemblySilverlight專案(沒有Web結尾的那一個)
SL011
SL012
另外,由於服務層使用XML,我們可以利用Silverlight 2.0所提供的LINQ To XML來簡化解譯XML的工作,LINQ To XML需要添加System.XML.Linq.dll為參考,請透過Add Reference來添加System.XML.Linq.dllSilverlight專案中(沒有Web結尾的那一個)
SL013
接著在Page.xaml中鍵入以下的XAML

 

Page.xaml
<UserControl x:Class="DBDemo1.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    Width="400" Height="300" Loaded="UserControl_Loaded">
    <Grid x:Name="LayoutRoot" Background="White" Height="300" Width="400">
        <data:DataGrid x:Name="grid" AutoGenerateColumns="True">
        </data:DataGrid>
    </Grid>
</UserControl>
最後在Page.xaml.cs中鍵入以下的程式碼。

 

Page.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.IO;
using System.Xml;
using System.Xml.Linq;
 
namespace DBDemo1
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
        }
 
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
                new Uri("http://localhost:40419/DBProvider.ashx", UriKind.Absolute));
            request.Method = "POST";
            request.BeginGetRequestStream(new AsyncCallback(ReadCallback),request);
        }
 
        private void ReadCallback(IAsyncResult state)
        {
            HttpWebRequest request = (HttpWebRequest)state.AsyncState;           
            Stream postStream = request.EndGetRequestStream(state);
            //byte[] buff = System.Text.Encoding.Unicode.GetBytes("TEST");
            //postStream.Write(buff, 0, buff.Length);
            postStream.Close(); // the request stream must closed before
            request.BeginGetResponse(new AsyncCallback(GetResponse), request);
        }
 
        private void GetResponse(IAsyncResult state)
        {
            HttpWebRequest request = (HttpWebRequest)state.AsyncState;
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(state);
            using (Stream stream = response.GetResponseStream())
            {
                XDocument doc = XDocument.Load(stream);
                Dispatcher.BeginInvoke(
                  new System.Threading.ParameterizedThreadStart(UpdateUI), doc);
            }
        }
 
       private void UpdateUI(object state)
        {
            XDocument doc = (XDocument)state;
            grid.ItemsSource = (from s1 in doc.Elements("Root").Descendants("Customer")
                                select new Customer()
                                    {
                                       ID = s1.Attribute("CUSTOMER_ID").Value,
                                       Name = s1.Attribute("CUSTOMER_NAME").Value
                                    }).ToList();
        }
    }
 
    public class Customer
    {
        public string ID { get; set; }
        public string Name { get; set; }
    }
}
SL014是此例執行結果。
SL014
透過HttpWebRequest的模式,我們可以用傳統網頁來扮演服務層,這可應用於多數網頁平台如PHPASPJSP之上。
 
使用WCF
 
   使用HTTPWebRequest的方式雖然簡單,但缺點是我們得自訂其間資料傳送的格式,最大化相容性的結果也限縮了結構性,所幸在現今網路世界中早已定義了多數網頁平台都支援的資料格式,那就是SOAP/Web ServiceSilverlight 2.0除了允許我們撰寫Web Service的客戶端外,同時也支援了.NET Framework 3.0中所新增的WCF Service,本例就以WCF Service做為服務層,請添加一個Web ServiceWeb結尾的專案中。
SL015
然後添加一個LINQ To SQL ClassesWeb結尾的專案中,做為取代ADO.NET的資料存取機制。
SL016
添加資料表至.DBML中。
SL017
刪除自動產生的IDBService.cs,然後於DBService.svc.cs中鍵入以下程式碼。

 

DBService.svc.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
 
namespace DBDemo1.Web
{
    [ServiceContract]
    public interface IDBService
    {
        [OperationContract]
        CUSTOMERS[] GetData();
    }
   
    public class DBService : IDBService
    {       
        public CUSTOMERS[] GetData()
        {
            using (MyDBDataContext context = new MyDBDataContext())
            {
                return (from s1 in context.CUSTOMERS select s1).ToArray();
            }
        }
    }
}
修改web.configserviceModel區段的設定。

 

web.config
............
<services>
   <servicebehaviorConfiguration="DBDemo1.Web.DBServiceBehavior"
               name="DBDemo1.Web.DBService">
    <endpointaddress=""binding="basicHttpBinding"contract="DBDemo1.Web.IDBService">
     <identity>
      <dnsvalue="localhost" />
     </identity>
    </endpoint>
    <endpointaddress="mex"binding="mexHttpBinding"contract="IMetadataExchange" />
   </service>
 </services>
............
Silverlight專案中,添加一Service Reference(服務參考)
SL018
SL019
添加一Silverlight UserControl
SL020
程式碼如下所示。

 

Page2.xaml
<UserControl x:Class="DBDemo1.Page2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    Width="400" Height="300" Loaded="UserControl_Loaded">
    <Grid x:Name="LayoutRoot" Background="White" Height="300" Width="400">
        <data:DataGrid x:Name="grid" AutoGenerateColumns="True">
        </data:DataGrid>
    </Grid>
</UserControl>
Page2.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;
 
namespace DBDemo1
{
    public partial class Page2 : UserControl
    {
        public Page2()
        {
            InitializeComponent();
        }
 
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {           
            ServiceReference1.DBServiceClient client =
                 new DBDemo1.ServiceReference1.DBServiceClient();
            client.GetDataCompleted +=
             new EventHandler<DBDemo1.ServiceReference1.GetDataCompletedEventArgs>(
               client_GetDataCompleted);
            client.GetDataAsync();
        }
 
        void client_GetDataCompleted(object sender,
                     DBDemo1.ServiceReference1.GetDataCompletedEventArgs e)
        {
            grid.ItemsSource = e.Result;
        }
    }
}
此例執行結果同前例。
 
使用ADO.NET Data Service Framework
 
   WCF/Web Service做為服務層雖然很完美,但設計師仍然得自行定義取得資料、查詢資料、更新資料的函式,在.NET Framework 3.5中,這些工作可以交給ADO.NET Data Service Framework來負責,這樣可以減輕設計師自行架構服務層規格的負擔,也能統一個別服務層的規格,避免不同設計師設計出不同的規格。預設情況下,ADO.NET Data Service Framework的資料層必須是ADO.NET Entity Framework,所以請於Web結尾的專案中添加一ADO.NET Entity Data Model項目。
SL021
SL022
SL023
 
SL024
完成後,在Web結尾的專案中添加一個ADO.NET Data Service項目。
SL025
WebDataService1.svc.cs中鍵入以下程式碼。

 

WebDataService1.svc.cs
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;
 
namespace DBDemo1.Web
{
    public class WebDataService1 : DataService< EntityData.MyDatabaseEntities>
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(IDataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.All);
            config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
        }
    }
}
完成後編譯專案,接著於Silverlight 2.0添加一Service Reference
SL026
然後於Silverlight專案中添加一新的Silverlight User Control

 

Page3.xaml
<UserControl x:Class="DBDemo1.Page3"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    Width="400" Height="300" Loaded="UserControl_Loaded">
    <Grid x:Name="LayoutRoot" Background="White" Height="300" Width="400">
        <data:DataGrid x:Name="grid" AutoGenerateColumns="True">
        </data:DataGrid>
    </Grid>
</UserControl>
Page3.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.Data.Services.Client;
 
namespace DBDemo1
{
    public partial class Page3 : UserControl
    {
        public Page3()
        {
            InitializeComponent();
        }
 
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            ServiceReference2.MyDatabaseEntities3 entities =
            new DBDemo1.ServiceReference2.MyDatabaseEntities3(
               new Uri("WebDataService1.svc",UriKind.Relative));
            var result = from s1 in entities.CUSTOMERS select s1;
            ((DataServiceQuery<ServiceReference2.CUSTOMERS>)result).BeginExecute(
                      new AsyncCallback(ReadCallback), result);
        }
 
        private void ReadCallback(IAsyncResult state)
        {
            DataServiceQuery<ServiceReference2.CUSTOMERS> svcCtx =
                (DataServiceQuery<ServiceReference2.CUSTOMERS>)state.AsyncState;
            //remember, you must call tolist,toarray before set to itemsource.
            grid.ItemsSource = svcCtx.EndExecute(state).ToList();
        }
    }
}
此例執行結果如前例。使用ADO.NET Data Service的好處在於能統一存取資料時的服務端規格,尤其在Silverlight 2.0所提供的ADO.NET Data Service Client Framework的幫助下,設計師還能輕鬆的更新資料庫,其間資料傳遞及異動機制都不需要設計師操心。
 
實作[偶而連線]模式
 
   或許你曾經聽過,Sivlerlight 不支援離線操作這檔事,事實的確是如此,但這指的是Silverlight不能像Flash般,包成一單一的執行檔來執行,透過IE等瀏覽器提供的離線模式,在沒有網路的情況下運行Silverlight程式依舊是可行的,前提是你的程式必須運用Silverlight 2.0中的Isolate Storage機制來儲存快取資料,而使用者至少需連線一次,讓相關的Silverlight程式下載到客戶端,下面的程式是一個可離線操作的簡單例子。

 

Page4.xaml
<UserControl x:Class="DBDemo1.Page4"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    Width="400" Height="300" Loaded="UserControl_Loaded">
    <Grid x:Name="LayoutRoot" Background="White" Height="300" Width="400">
        <data:DataGrid x:Name="grid" AutoGenerateColumns="True">
        </data:DataGrid>
    </Grid>
</UserControl>
Page4.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.IO;
using System.IO.IsolatedStorage;
using System.Xml.Linq;
 
 
namespace DBDemo1
{
    public partial class Page4 : UserControl
    {
        public Page4()
        {
            InitializeComponent();
        }
 
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
                 new Uri("http://localhost:40419/DBProvider.ashx", UriKind.Absolute));
            request.Method = "POST";
            request.BeginGetRequestStream(new AsyncCallback(ReadCallback), request);
        }
 
        private void ReadCallback(IAsyncResult state)
        {
            HttpWebRequest request = (HttpWebRequest)state.AsyncState;
            Stream postStream = request.EndGetRequestStream(state);
            //byte[] buff = System.Text.Encoding.Unicode.GetBytes("TEST");
            //postStream.Write(buff, 0, buff.Length);
            postStream.Close(); // the request stream must closed before
            request.BeginGetResponse(new AsyncCallback(GetResponse), request);
        }
 
        private void GetResponse(IAsyncResult state)
        {
            try
            {
                HttpWebRequest request = (HttpWebRequest)state.AsyncState;
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(state);
                using (Stream stream = response.GetResponseStream())
                {
                    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                    {
                        store.CreateDirectory("Data");
                        IsolatedStorageFileStream subDirFile =
                          store.CreateFile(System.IO.Path.Combine("Data", "dbdata.txt"));
                        using (StreamWriter sw = new StreamWriter(subDirFile))
                        {
                            using(StreamReader sr = new StreamReader(stream))
                            {
                                sw.Write(sr.ReadToEnd());
                            }
                        }
 
                    }
                    XDocument doc = XDocument.Load(stream);
                    Dispatcher.BeginInvoke(
                       new System.Threading.ParameterizedThreadStart(UpdateUI), doc);
                }
            }
            catch (Exception ex)
            {
                using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    IsolatedStorageFileStream subDirFile =
                store.OpenFile(System.IO.Path.Combine("Data", "dbdata.txt"),FileMode.Open);
                    XDocument doc = XDocument.Load(subDirFile);
                    subDirFile.Close();
                    Dispatcher.BeginInvoke(
                  new System.Threading.ParameterizedThreadStart(UpdateUI), doc);
                }
            }
        }
 
        private void UpdateUI(object state)
        {
            XDocument doc = (XDocument)state;
            grid.ItemsSource = (from s1 in doc.Elements("Root").Descendants("Customer")
                                select new Customer()
                                {
                                    ID = s1.Attribute("CUSTOMER_ID").Value,
                                    Name = s1.Attribute("CUSTOMER_NAME").Value
                                }).ToList();
        }
    }
}
適當使用Isolate Storage來儲存資料快取,可以讓應用程式減少網路流量,達到提升效率的目的。
SL027
 
Next Time...
 
 下次有機會的話,我們再來討論較進階的資料庫應用程式撰寫技巧,例如異動資料、查詢及處理關聯等等。