維's profileIT : 是工作還是嗜好?PhotosBlogListsMore Tools Help

維 李

There are no music lists on this space.
謝謝您的瀏覽!
Please wait...
Sorry, the comment you entered is too long. Please shorten it.
You didn't enter anything. Please try again.
Sorry, we can't add your comment right now. Please try again later.
To add a comment, you need permission from your parent. Ask for permission
Your parent has turned off comments.
Sorry, we can't delete your comment right now. Please try again later.
You've exceeded the maximum number of comments that can be left in one day. Please try again in 24 hours.
Your account has had the ability to leave comments disabled because our systems indicate that you may be spamming other users. If you believe that your account has been disabled in error please contact Windows Live support.
Complete the security check below to finish leaving your comment.
The characters you type in the security check must match the characters in the picture or audio.
維 李wrote:
>Delphi2010附带的Indy10.5.5对IPv6的支持有问题。经测试问题出在TIdStackWindows.SupportsIPv6函数上,至少在Windows7中

Indy的問題應該直接反映給Indy團隊, Delphi R&D團隊並不直接負責Indy的bug修改.
1 minute ago
Ouwrote:
Delphi2010附带的Indy10.5.5对IPv6的支持有问题。经测试问题出在TIdStackWindows.SupportsIPv6函数上,至少在Windows7中,当WSAEnumProtocols的前两个参数都是nil时,是返回SOCKET_ERROR(-1)结果的,但是能够正确返回第3个参数即LLen变参。这样“if IdWinsock2.WSAEnumProtocols(nil, nil, LLen) > 0 then”中的条件肯定不会成立,导致TIdStackWindows.SupportsIPv6函数返回False,即不支持IPv6,而这与Windows7的缺省配置完全不符。正确的做法是将上述if判断去掉,直接调用IdWinsock2.WSAEnumProtocols(nil, nil, LLen),然后直接执行原if语句begin/end中的内容。其实原来的Indy10.2.3中TIdStackWindows.SupportsIPv6的处理是完全正确的,在XP/Vista/Windows7上都没有问题,不知为什么Indy10.5.x给改错了。
3 days ago
維 李wrote:
>李老師,Diablo III已經推出了唷!嘿嘿…

應該還沒有吧, 只是前陣子公佈了第4個武僧的職業, 我在想推出的時間會不會是明年的聖誕節.啊, 實在是等太久了.
16 Nov.
RUwrote:
李老師,Diablo III已經推出了唷!嘿嘿…
官網在這
13 Nov.
維 李wrote:
>哈!我将工程从C++Builder 2009转到C++Builder2010下,结果经常冒出来F1004错误。

如果你可以提供一個可以產生F1004的cpp或是表單, 我可以幫你問問EMBT中我認識的R&D的人
11 Nov.

IT : 是工作還是嗜好?

November 11

RAD Studio 2010, Delphi 2010, C++Builder 2010 Update 2已可下載

CodeGear已釋出 RAD Studio 2010, Delphi 2010和C++Builder 2010的Update 2, 我已經下載安裝完畢, 整個過程非常的順利.

如果您是合法的使用者的話, 那麼您可以在下面的URL下載

October 23

JSON 程式設計 – DataSnap的回叫機制

DataSnap 2010加入了回叫機制,當伺服端方法在執行的過程中可以回叫用戶端提供的方法以通知用戶端有關伺服端方法執行的狀態。

DataSnap的回叫機制非常適合使用在需要較長時間執行的伺服端方法,例如如果伺服端方法需要執行長時間的查詢時就很適合使用,或是當程式啟動或是執行時需要進行許多查詢的工作,那麼也都可以使用回叫機制。

要使用DataSnap 2010的回叫機制非常的簡單,開發人員只需要實作一個從TDBXCallback繼承下來的實體類別,並且在呼叫伺服端方法時把此實體類別的樣例當做參數傳遞給伺服端方法即可。

由於使用DataSnap 2010的回叫機制並不困難,因此讓我們使用一個範例來說明讀者就可以很快的瞭解。

在下列的範例中本文將使用Delphi 2010中新的IOUtils程式單元中的類別進行檔案搜尋和計數的工作,由於這將花上一些時間,因此我們正好使用它來展示使用同步和回叫機制的差異。

範例DataSnap伺服器

首先讓我們建立一個DataSnap伺服端,下面是這個伺服器輸出的伺服端方法,請注意的是,TServerMethods1輸出了兩個方法GetServerDirectoryInfo和GetServerDirectoryInfoAsync。這兩個方法都執行相同的工作,它們使用TDirectory類別搜尋和計數特定伺服端目錄下的檔案總數,它們的差異是GetServerDirectoryInfo使用同步的方式執行,因此當用戶端呼叫它時,用戶端會同時暫停反應直到GetServerDirectoryInfo執行完畢。

而GetServerDirectoryInfoAsync則是使用回叫機制的方式執行,因此當用戶端呼叫它之後,GetServerDirectoryInfoAsync在執行的過程中則可以藉由用戶端傳遞來的參數ACallback: TDBXCallback,來回叫回用戶端,告訴用戶端執行的狀態,用戶端因此也根據目前伺服端執行的情形來更新用戶端的資訊。

{$METHODINFO ON}

TServerMethods1 = class(TPersistent)

private

{ Private declarations }

fTotalFiles : Integer;

FResult : TJSONArray;

FCallback: TDBXCallback;

procedure ProcessPath(const sPath : string);

procedure ProcessPathAsync(const sPath : string);

procedure ShowMessage(sMessage : string);

procedure ProcessThisDirectory(const sPath : string);

public

{ Public declarations }

function EchoString(Value: string): string;

function GetServerDirectoryInfo(const sPath : string) : TJSONArray;

function GetServerDirectoryInfoAsync(ACallback: TDBXCallback; const sPath : string): TJSONArray;

end;

{$METHODINFO OFF}

GetServerDirectoryInfoAsync是如何回叫回用戶端呢?其實非常的簡單,因為用戶端在呼叫它時已經把用戶端的回叫方法當成參數傳遞過來了,因此GetServerDirectoryInfoAsync只需要藉由這個參數即可回叫回用戶端。

因此我們可以從下面18行的程式碼看到,伺服端直接使用這個回叫參數呼叫用戶端,並且建立一個TJSONString型態的物件做為參數,在這個TJSONString物件中告訴了用戶端目前伺服端正在處理那一個目錄。

001 function TServerMethods1.GetServerDirectoryInfoAsync(ACallback: TDBXCallback;

002 const sPath: string): TJSONArray;

003 begin

004     FCallback := ACallBack;

005     FTotalFiles := 0;

006     FResult := TJSONArray.Create;  

007     ProcessPathAsync(sPath);

008     FResult.AddElement(TJSONString.Create('總檔案數' + ' : ' + IntToStr(FTotalFiles)));

009     Result := FResult;

010 end;

011

012 procedure TServerMethods1.ProcessPathAsync(const sPath: string);

013 var

014     rootDirectories : TStringDynArray;

015     i: Integer;

016 begin

017     ProcessThisDirectory(sPath);

018     FCallback.Execute(TJSONString.Create('處理目錄' + sPath + '中...'));

019     rootDirectories := TDirectory.GetDirectories(sPath);

020     for i := 0 to Length(rootDirectories) - 1 do

021     ProcessPathAsync(rootDirectories[i]);

022 end;

同步用戶端

範例的同步用戶端非常的簡單,它只是直接呼叫伺服端的GetServerDirectoryInfo方法。

procedure TForm3.btnGetServerInfoClick(Sender: TObject);

var

    aServer : TServerMethods1Client;

    ja : TJSONArray;

    jv : TJSONValue;

    I: Integer;

begin

    lStart := GetTickCount;

    aServer := TServerMethods1Client.Create(Self.SQLConnection1.DBXConnection);

    try

        ja := aServer.GetServerDirectoryInfo(Edit1.Text);

        lEnd := GetTickCount;

        for I := 0 to ja.Size - 1 do

        begin

            jv := ja.Get(I);

            lbResult.Items.Add(jv.ToString);

        end;

    finally

        aServer.Free;

        ShowRunTime(lEnd - lStart);

    end;

end;

在用戶端呼叫GetServerDirectoryInfo的過程中,用戶端暫停反應,使用者也無從瞭解伺服端執行的狀態。

回叫用戶端

再看看回叫用戶端,這個用戶端的關鍵從下面的012行開始,012行建立了TDSCallbackWithMethod物件,並且建立一個匿名方法做為用戶端的回叫方法傳遞給伺服端。從013行開始的匿名方法在被用戶端回叫的時候首先在013行把伺服端傳遞來的參數型態轉換為TJSONString的型態,接著更新用戶端的UI以通知使用者伺服端目前正在處理那一個目錄。最後在023行用戶端回叫方法如果執行成功就需要回傳TJSonTrue物件,如果失敗的話就需要回傳TJSonFalse物件。

001 procedure TForm3.btnGetServerInfoAsyncClick(Sender: TObject);

002 var

003     aServer : TServerMethods1Client;

004     LCallback : TDSCallbackWithMethod;

005     ja : TJSONArray;

006     jv : TJSONValue;

007     I: Integer;

008 begin

009     lStart := GetTickCount;

010     aServer := TServerMethods1Client.Create(Self.SQLConnection1.DBXConnection);

011     try

012         LCallback := TDSCallbackWithMethod.Create(

013             function(const Args: TJSONValue): TJSONValue

014             var

015                 asyncResult: TJSONString;

016                 I: Integer;

017                 LMessage: string;

018             begin

019                 asyncResult := TJSONString(Args);

020                 lbAsync.Items.Add(asyncResult.ToString);

021                 lbAsync.Update;

022                 Application.ProcessMessages;

023                 Result := TJSonTrue.Create;

024              end

025 );

026         ja := aServer.GetServerDirectoryInfoAsync(LCallback, Edit1.Text);

027

028

029         lEnd := GetTickCount;

030         for I := 0 to ja.Size - 1 do

031         begin

032             jv := ja.Get(I);

033             lbResult.Items.Add(jv.ToString);

034         end;

035     finally

036         aServer.Free;

037         ShowRunTime(lEnd - lStart);

038     end;

039 end;

那麼什麼是TDSCallbackWithMethod類別呢? 這要從TDBXCallback抽象類別開始談起。

TDBXCallback抽象類別

在討論TDSCallbackMethod之前我們必須先說明TDBXCallback抽象類別,因為TDSCallbackMethod是從TDBXCallback繼承下來的實體類別。事實上TDBXCallback類別即是使用DataSnap 2010回叫機制的關鍵,要使用回叫機制,開發人員必須實作一個從TDBXCallback繼承下來的實體類別,並且傳遞此實體類別的樣例給伺服端方法做為參數,如此一來伺服端方法就可以藉由這個樣例參數呼叫回用戶端,以通知用戶端伺服端方法執行的狀態。

TDBXCallback的虛擬方法Execute是衍生類別需要複載實作的,Execute接受一個型態為TJSONValue的參數並且回傳一個型態為TJSONValue的結果值,伺服端方法在回叫用戶端的方法時,可以把需要傳遞給用戶端的數值或是物件轉換為TJSONValue型態並且當成Execute方法的參數傳遞回用戶端,而用戶端的方法在被回叫執行完畢之後,也可以把執行結果轉換為TJSONValue型態並且回傳給伺服端。下面即是TDBXCallback抽象類別的宣告:

001 TDBXCallback = class abstract

002 public

003 function Execute(const Arg: TJSONValue): TJSONValue; virtual; abstract;

004 protected

005 procedure SetConnectionHandler(const ConnectionHandler: TObject); virtual;

006 procedure SetOrdinal(const Ordinal: Integer); virtual;

007 public

008 property ConnectionHandler: TObject write SetConnectionHandler;

009 property Ordinal: Integer write SetOrdinal;

010 end;

TDSCallbackMethod實體類別

瞭解了TDBXCallback扮演的角色之後解釋TDSCallbackMethod實體類別就簡單了,由於此範例應用程式要使用非同步呼叫機制,因此宣告TDSCallbackMethod從TDBXCallback繼承下來並且實作虛擬方法Execute。在TDSCallbackMethod的Execute方法中它只是簡單的呼叫在建構函式中儲存下來的用戶端方法的方法指標。

unit AsyncUtils;

interface

uses

Classes,

DbxDatasnap,

DBXJson;

type

TDSCallbackMethod = reference to function(const Args: TJSONValue): TJSONValue;

TDSCallbackWithMethod = class(TDBXCallback)

private

    FCallbackMethod: TDSCallbackMethod;

public

    constructor Create(ACallbackMethod: TDSCallbackMethod);

    function Execute(const Args: TJSONValue): TJSONValue; override;

end;

implementation

constructor TDSCallbackWithMethod.Create(ACallbackMethod: TDSCallbackMethod);

begin

    FCallbackMethod := ACallbackMethod;

end;

function TDSCallbackWithMethod.Execute(const Args: TJSONValue): TJSONValue;

var

aString: string;

begin

    Assert(Assigned(FCallbackMethod));

    Result := FCallbackMethod(Args);

end;

end.

下面的圖形分別是DataSnap伺服端執行的畫面以及同步用戶端,回叫用戶端的執行畫面,從圖2同步用戶端畫面可以看到它雖然比回叫用戶端執行的快(在筆者的機器中執行了7.078秒),但是在同步用戶端呼叫DataSnap伺服器時它的整個UI是暫停的,因此它無法在表單下方的ListBox中顯示任何伺服端執行的資訊,使用者必須等待它完全執行完畢才能取回用戶端應用程式的控制權。

image

圖1 DataSnaps伺服器的執行畫面

image

圖2 同步客戶端的執行畫面

image

圖3 回客戶端的執行畫面

相反的畫面3則是回叫用戶端的執行結果,我們看到它是比同步用戶端慢(在筆者的機器中執行了12.984秒),但是在整個執行過程中使用者仍然可以控制用戶端應用程式,而且回叫用戶端能夠不停的在表單下方的ListBox中顯示目前伺服端正在處理的目錄資訊。因此就使用者經驗來說,回叫用戶端是比同步用戶端好多了。

也許可以更好

DataSnap 2010的回叫機制雖然使用上非常的簡單,但在許多的應用中卻仍然可能不便,例如現在的機制需要我們在把用戶端回叫方法傳遞給伺服端方法當成參數,但這實在有些囉嗦,因為如果用戶端有許多的回叫方法,那麼在每次呼叫伺服端方法時都需要傳遞一次。因此如果DataSnap 2010能夠提供一個全域的回叫方法註冊機制,讓用戶端只需要為每一個回叫方法註冊一次即可,而無需每次呼叫即傳遞一次,那麼在使用上將更為簡化。不過DataSnap 2010的確是一個大幅進步的版本,相信未來Delphi/BCB團隊會繼續的增強DataSnap的功能。

Have Fun!

September 28

JSON 程式設計 – 使用過濾器

在以前Tiburon遊記的文章解釋過JSON是使用字串型態來傳遞資料的,因此所有其他型態的資料都必須轉換為字串的型態,雖然如此一來在處理上比較簡單,但這也造成了其他的問題,例如一些敏感性的資料如果使用字串型態來傳遞的話就會有問題。DataSnap 2010為了解決這種問題因此加入了過濾器的機制,讓開發人員在傳遞特殊的資料時可以藉由過濾器來進行額外的處理,例如在傳遞資料出去時先加密,並且在接受到資料之後再進行解密。本篇文章的目的即在於討論如何使用DataSnap 2010的過濾器。

使用DataSnap 2010的過濾器非常的簡單,Delphi 2010也內建了一個壓縮過濾器,可以有效的壓縮使用TCP/IP通訊協定的資料傳遞。讓我們先說明如何使用這個內建的過濾器,稍後我們再深入的說明如何開發客製化過濾器。

讓我們仍然使用上篇文章的範例,要使用過濾器,請開啟ServerContainerUnit程式單元,點選表單中的TDSTCPServerTransport元件,並且雙擊它的Filters特性,此時Filters特性值編輯器會啟動,請於其中加入一個新的過濾器,點選此新的過濾器,然後在物件檢視器中選擇它的FilterId為ZlibCompression,如下所示:

clip_image002

ZlibCompression過濾器就是DataSnap 2010內建的壓縮過濾器,在加入了ZlibCompression過濾器之後,編譯並且執行範例DataSnap伺服器,現在DataSnap伺服器就提供了壓縮JSON資料的能力。

現在再讓我們開啟用戶端應用程式,因為我們要在用戶端應用程式中加入解壓縮資料的能力,這個非常的簡單,我們只要在用戶端應用程式的主表單中加入使用DBXCompressionFilter程式單元即可,例如下面就是用戶端應用程式加入DBXCompressionFilter程式單元的程式碼:

implementation

uses DBXJSONReflect, DBXJSON, uServerProxy, uEmployee, DBXCompressionFilter;

現在編譯並且執行用戶端應用程式,並且讓我們使用TCP Viewer來觀察使用壓縮過濾器之前的情形以及使用壓縮過濾器之後的效果。

下圖是TCP Viewer顯示範例DataSnap應用系統使用壓縮過濾器之前的情形,從下圖中我們可以看到在DataSnap伺服器和用戶端應用程式之間傳遞的資料當然是使用字串的型態,所有傳遞的資料都一清二楚,同時請讀者注意下圖右邊顯示了從伺服器傳遞到用戶端的資料量(938位元組)以及從用戶端傳遞到伺服端的資料量(706位元組)。

clip_image004

而下圖則是使用壓縮過濾器之後的效果:

clip_image006

從上圖中可以看到傳遞的資料經過壓縮,因此不易看出原始的資料,而且請讀者注意下圖右邊顯示了從伺服器傳遞到用戶端的資料量(653位元組)以及從用戶端傳遞到伺服端的資料量(602位元組),可見到壓縮過濾器有效的減少了伺服器和用戶端之間的資料傳遞量,這不但可以增加分散式應用程式的執行速度,也可以增加支援的用戶端的數量。

如何? 使用過濾器是不是又簡單,又有明顯的效果? 不過DataSnap 2010只提供了一個內建的過濾器實在太少,好在DataSnap 2010過濾器架構在設計時就考慮到了允許讓開發人員能夠自行開發過濾器並且內嵌到DataSnap之中,接下來筆者將討論如何開發客製化過濾器並且使用在DataSnap 2010的分散式應用系統中。

開發客製化過濾器

要開發客製化過濾器,開發人員必須從TTransportFilter類別衍生子代類別並且實作TTransportFilter類別中相關的虛擬方法,下面的表單說明了開發人員需要實作的虛擬方法:

函式名稱

回傳型態

說明

GetParameters

TDBXStringArray

回傳所有的參數

GetUserParameters

TDBXStringArray

回傳使用者可改變的參數

ProcessInput

TBytes

使用客製化程式碼正向處理傳遞的資料流

ProcessOutput

TBytes

使用客製化程式碼反向處理傳遞的資料流

Id

UnicodeString

過濾器的ID

GetParameterValue

UnicodeString

取得特定名稱的參數值

SetParameterValue

Boolean

設定特定名稱的參數值

瞭解了需要實作那些虛擬方法之後,我們就可以開始動手開發一個客製化過濾器了。在本文中筆者將撰寫一個非常簡單的加密/解密過濾器,其實這個加密/解密過濾器只是在傳遞資料時和一個字串進行xor的動作,到了另一端再次xor相同的字串而已。

首先讓我們宣告範例TTransportEncryptFilter類別從TTransportFilter類別繼承下來並且複載相關必要的虛擬方法:

TTransportEncryptFilter = class(TTransportFilter)

private

FEncrypt: TSimpleEncryptor;

FParameters: TDictionary<String, String>;

protected

function GetParameters: TDBXStringArray; override;

function GetUserParameters: TDBXStringArray; override;

public

function GetParameterValue(const ParamName: UnicodeString): UnicodeString;

override;

function SetParameterValue(const ParamName: UnicodeString;

const ParamValue: UnicodeString): Boolean; override;

constructor Create; override;

destructor Destroy; override;

function ProcessInput(const Data: TBytes): TBytes; override;

function ProcessOutput(const Data: TBytes): TBytes; override;

function Id: UnicodeString; override;

end;

TTransportEncryptFilter類別將使用TSimpleEncryptor進行字串xor的運算,在TSimpleEncryptor類別中實作了兩個方法,Encrypt和Decrypt,其實這兩個方法的實作程式碼是一樣的,只是為了說明方便分別實作成Encrypt和Decrypt以便讓讀者瞭解。Encrypt和Decrypt接受TBytes型態的參數,這個參數在Encrypt方法中是代表傳遞出去的資料,Encrypt方法使用程式碼加密之後再把加密過的資料以TBytes型態回傳。

而Decrypt的參數則是代表接受來的資料,Decrypt方法必須使用程式碼加以解密以還原資料。

TSimpleEncryptor = class

protected

public

function Encrypt(const Data: TBytes): TBytes;

function Decrypt(const Data: TBytes): TBytes;

constructor Create;

end;

下面是這兩個方法的實作程式碼,讀者可以看到這兩個方法的實作程式碼是 樣的,它們都接受的參數以'DexterHighlanderTiburonWeaver'這個鍵值字串進行xor的運算:

const

EncryptKey = 'DexterHighlanderTiburonWeaver';

constructor TSimpleEncryptor.Create;

begin

inherited Create;

end;

function TSimpleEncryptor.Decrypt(const Data: TBytes): TBytes;

var

i: Integer;

idx: Integer;

begin

Result := Data;

idx := 0;

for i := 0 to Length(Data) - 1 do

begin

Result[i] := Byte(Chr(Ord(Data[i]) xor Ord(EncryptKey[idx])));

Inc(idx);

if (idx > Length(EncryptKey)) then

idx := 0;

end;

end;

function TSimpleEncryptor.Encrypt(const Data: TBytes): TBytes;

var

i: Integer;

idx: Integer;

begin

Result := Data;

idx := 0;

for i := 0 to Length(Data) - 1 do

begin

Result[i] := Byte(Chr(Ord(Data[i]) xor Ord(EncryptKey[idx])));

Inc(idx);

if (idx > Length(EncryptKey)) then

idx := 0;

end;

end;

下面則是TTransportEncryptFilter類別的實作程式碼,讀者可以看到在在建構函式中建立了TSimpleEncryptor物件,並且分別在ProcessInput虛擬方法中呼叫TSimpleEncryptor物件的Encrypt方法加密傳遞的資料並且在ProcessOutput虛擬方法中呼叫TSimpleEncryptor物件的Decrypt方法以解密資料:

function TTransportEncryptFilter.GetUserParameters: TDBXStringArray;

begin

SetLength(Result, 1);

Result[0] := EncryptKey;

end;

function TTransportEncryptFilter.GetParameters: TDBXStringArray;

begin

SetLength(Result, 1);

Result[0] := EncryptKey;

end;

function TTransportEncryptFilter.GetParameterValue

(const ParamName: UnicodeString): UnicodeString;

begin

FParameters.TryGetValue(ParamName, Result);

if ( ParamName = EncryptKey ) and ( Result = '' ) then

Result := '0';

end;

function TTransportEncryptFilter.SetParameterValue

(const ParamName, ParamValue: UnicodeString): Boolean;

begin

FParameters.AddOrSetValue(ParamName, ParamValue);

Result := True;

end;

constructor TTransportEncryptFilter.Create;

begin

inherited Create;

FParameters := TDictionary<String, String>.Create;

FEncrypt := TSimpleEncryptor.Create;

end;

destructor TTransportEncryptFilter.Destroy;

begin

FreeAndNil(FParameters);

FreeAndNil(FEncrypt);

inherited Destroy;

end;

function TTransportEncryptFilter.ProcessInput(const Data: TBytes): TBytes;

begin

OutputDebugString(PWideChar('Encrypted - ' + Stringof(Data)));

Result := FEncrypt.Encrypt(Data);

end;

function TTransportEncryptFilter.ProcessOutput(const Data: TBytes): TBytes;

begin

OutputDebugString(PWideChar('Decrypted - ' + Stringof(Data)));

Result := FEncrypt.Decrypt(Data);

end;

function TTransportEncryptFilter.Id: UnicodeString;

begin

Result := EncryptFilterName;

end;

最後我們需要在initialization部份註冊這個客製化過濾器並且在finalization部份解除註冊客製化過濾器:

initialization

TTransportFilterFactory.RegisterFilter(EncryptFilterName,

TTransportEncryptFilter);

finalization

TTransportFilterFactory.UnregisterFilter(EncryptFilterName);

使用客製化過濾器

OK,回到範例伺服器,開啟ServerContainer程式單元並且在它的OnCreate事件處理函式中使用TDSTCPServerTransport的AddFilter方法加入我們的客製化過濾器:

procedure TServerContainer1.DataModuleCreate(Sender: TObject);

var

i : Integer;

begin

i := DSTCPServerTransport1.Filters.AddFilter(uEncryptFilter.EncryptFilterName);

for i := 0 to DSTCPServerTransport1.Filters.Count - 1 do

Form1.lbFilters.Items.Add(DSTCPServerTransport1.Filters.GetFilter(i).Id);

end;

當然我們也需要在ServerContainer程式單元的uses句子中加入包含客製化過濾器的程式單元uEncryptFilter:

implementation

uses Windows, ServerMethodsUnit1, MainForm, uEncryptFilter;

現在執行DataSnap伺服器,我們就可以看到伺服器顯示它已經找到了我們的客製化過濾器:

clip_image008

接著開啟用戶端應用程式,在主表單中也加入客製化過濾器的程式單元uEncryptFilter,編譯並且執行用戶端應用程式,再使用TCP Viewer觀察傳遞的資料,我們果然看到資料現在都經過加密了:

clip_image010

但是我們可以看到傳遞的資料量增加了。

但是用戶端仍然可以正確的接受到資料:

clip_image012

當然我們也可以同時使用兩個過濾器,享受加密又壓縮的好處,下圖是伺服器同時支援了兩個過濾器:

clip_image014

如果我們再次使用TCP Viewer,就可以看到下圖,享受加密又壓縮的好處,因為資料加密了而且資料傳遞量又減少了。

clip_image016

現在您應該瞭解了如何使用DataSnap 2010的過濾器功能以及如何開發客製化過濾器,我們下次再討論DataSnap 2010的非同步機制。

Have Fun!

September 23

JSON 程式設計 – Marshaling/Unmarshaling

上篇文章中說明了如何使用Delphi 2010中通用的JSON相關類別來進行程式設計並且以一個TEmployee類別做為範例來說明如何把TEmployee物件轉換為JSON格式的資料並且藉由DataSnap傳遞到用戶端。

由於類似這種傳遞Value Object的應用在JSON程式設計的世界被非常廣泛的應用,因此DataSnap 2010正式加入了JSON Marshaling/Unmarshaling的功能,允許程式師把物件轉換為符合JSON格式的序列化物件傳遞,例如從伺服端傳遞到用戶端。這樣做的好處是只要支援JSON的技術都可以自由的解析其中的資料並且加入處理,如此一來DataSnap 2010的JSON Marshaling/Unmarshaling技術就可以達成和同質或是異質平台互動的能力,也就是說DataSnap 2010的JSON Marshaling/Unmarshaling技術是已經是一個跨平台的分散式架構了。

使用DataSnap 2010的JSON Marshaling/Unmarshaling非常的簡單,因為對大多數的物件而言,DataSnap 2010能夠自動的把它轉換為TJSONObject型態的物件,然後再加以傳遞,因此開發人員只需要撰寫非常簡易的程式碼就可以輕易的完成。現在讓我們仍然使用上篇文章中的TEmployee類別做為說明,看看使用JSON Marshaling/Unmarshaling功能之後在分散式環境中傳遞物件是如何的容易。

建立DataSnap伺服器

Delphi 2010提供了新的DataSnap精靈幫助開發人員快速建立DataSnap伺服器,如此一來我們就不必每次都不斷重覆的放入TDSServer和TDSTCPServerTransport等元件,可節省我們的開發時間。此外DataSnap 2010也開始支援HTTP/HTTPS通訊協定,DataSnap精靈允許開發人員自動設定更多的元件組態資訊。

啟動Delphi 2010,點選File|New啟動New Items對話盒並且於DataSnap Server分類中選擇DataSnap Server圖像以建立DataSnap伺服器:

Image1

接著在New DataSnap Server對話盒中讓我們建立一般的VCL Forms應用程式做為DataSnap伺服器的型態,選擇使用TCP/IP和HTTP/HTTPS通訊協定,並且選擇使用TDSServerModule做為類別做為輸出服務的父代類別,如下所示:

Image2

點選OK按鈕之後Delphi 2010便會為自動我們建立如下的ServerContainerUnit1程式單元,其中即包含了所有必要的元件。其中的TDSHTTPService和TDSHTTPServiceAuthenticationManager是新的元件,這兩個元件是使用來支援HTTP/HTTPS通訊協定的。

Image3

現在讓我們把Unit1程式單元儲存為名為MainForm的程式單元:

Image4

接著點選TDSHTTPService元件,設定它的HttpPort特性值為8080,因為通信埠80已經被Web伺服器使用,因此在這個範例中我使用8080,如果通信埠8080在讀者的機器中已經被使用,那麼您可以使用任何尚未使用的通信埠。請注意TDSHTTPService的RESTContext特性,您可以看到它的特性值是Rest,這代表Delphi 2010的DataSnap伺服器一旦使用TDSHTTPService元件就自動支援RESTful的功能,我們稍後再討論RESTful。

Image5

由於Delphi 2010強化了RTTI提供Reflection的機制,因此DataSnap 2010允許開發人員選擇使用TDSServerModule,TDataModule或是TPersistent類別做為輸出服務的父代類別,因為Delphi 2010都可以藉由RTTI從任何父代類別取得必要的資訊。

現在讓我們在這個DataSnap伺服器專案中加入上篇文章中的TEmployee程式單元,並且輸出GetEmployee方法讓用戶端能夠從伺服端取得TEmployee物件。請注意GetEmployee回傳TJSONValue型態的數值,由於DataSnap 2010在參數列和回傳值都開始支援TJSONValue,因此這代表只要我們把任何物件轉換為TJSONValue物件,就可以自由傳遞到用戶端,或是從用戶端傳遞回伺服端,而這個流程就是DataSnap 2010的JSON Marshaling/Unmarshaling技術。

TServerMethods1 = class(TDSServerModule)

private

{ Private declarations }

public

{ Public declarations }

function EchoString(Value: string): string;

function GetEmployee : TJSONValue;

end;

要使用JSON Marshaling/Unmarshaling,請加入使用DBXJSONReflect程式單元,如下004行所示。

要以JSON格式傳遞物件非常的簡單,首先我們需要建立我們要傳遞的物件,一個TJSONMarshal物件以及一個TJSONConverter物件。因此下面的011行先建立TEmployee物件,012行建立TJSONMarshal物件,並且在它的建構函式中傳入一個TJSONConverter物件,最後我們只需要呼叫TJSONMarshal物件的Marshal虛擬方法並且傳入要傳遞的物件即可,如014行所示:

01 implementation

002

003 {$R *.dfm}

004 uses DBXJSONReflect, uEmployee;

005

006 function TServerMethods1.GetEmployee: TJSONValue;

007 var

008 anEmployee : TEmployee;

009 aMarshaler : TJSONMarshal;

010 begin

011 anEmployee := TEmployee.Create('王大明', 'wtm@somemail.com', '123456');

012 aMarshaler := TJSONMarshal.Create(TJSONConverter.Create);

013 try

014 Result := aMarshaler.Marshal(anEmployee);

015 finally

016 FreeAndNil(anEmployee);

017 FreeAndNil(aMarshaler);

018 end;

019 end;

如此一來TJSONMarshal和TJSONConverter類別就可以自動把傳入的物件轉換為JSON的格式並且傳遞出去。

建立DataSnap用戶端

現在建立一個VCL Form應用程式專案,放入TSQLConnection元件,連結到DataSnap伺服器(如果您不知道如何做,請參考筆者的Tiburon遊記系列文章),建立代表DataSnap伺服器輸出服務的用戶端程式單元uServerProxy,同樣把TEmployee程式單元加入到用戶端專案中,再參考DBXJSONReflect和DBXJSON程式單元,如下001行所示。

在用戶端要反轉JSON物件回正確物件的流程就是Unmarshaling,我們只需要在013行建立TJSONUnMarshal物件,015行呼叫DataSnap伺服器的GetEmployee服務方法,GetEmployee回傳TJSONValue型態的物件,016行先存取回傳的TJSONValue物件的JSON格式文字值,接著018行呼叫TJSONUnMarshal的Unmarshal方法,傳入回傳的TJSONValue物件並且使用as運算元把它正確的轉換回TEmployee物件,之後我們就可以自由的存取TEmployee物件的特性值了。

001 uses DBXJSONReflect, DBXJSON, uServerProxy, uEmployee;

002

003 {$R *.dfm}

004

005 procedure TForm2.Button1Click(Sender: TObject);

006 var

007 aServer : TServerMethods1Client;

008 unMarshaler : TJSONUnMarshal;

009 aValue : TJSONValue;

010 anEmployee : TEmployee;

011 begin

012 aServer := TServerMethods1Client.Create(Self.SQLConnection1.DBXConnection);

013 unMarshaler := TJSONUnMarshal.Create;

014 try

015 aValue := aServer.GetEmployee;

016 Edit4.Text := aValue.ToString;

017

018 anEmployee := unMarshaler.Unmarshal(aValue) as TEmployee;

019 Edit1.Text := anEmployee.Name;

020 Edit2.Text := anEmployee.EMail;

021 Edit3.Text := anEmployee.Phone;

022

023 finally

024 FreeAndNil(anEmployee);

025 FreeAndNil(unMarshaler);

026 FreeAndNil(aServer);

027 end;

028 end;

從下圖可以看到TEmployee物件的確可以正確的從DataSnap伺服器傳遞到用戶端,當然用戶端也可以修改數值之後再傳遞回DataSnap伺服器再更新回資料庫。

Image6

使用DataExplorer存取DataSnap伺服器的服務

由於DataSnap 2010自動支援RESTful架構,因此既然在前面我們選擇讓DataSnap伺服器支援HTTP/HTTPS通訊協定,因此我們現在就可以使用瀏覽器來存取這個DataSnap伺服器的服務,我們只需要使用下面的樣例就可以存取到DataSnap伺服器的服務:

http://server url/datasnap/rest/服務程式單元/服務方法

https://server url/datasnap/rest/服務程式單元/服務方法

因此要存取前面的DataSnap伺服器,我們只需要使用下面的URL:

http://localhost:8080/datasnap/rest/TServerMethods1/GetEmployee

使用8080當然是因為我們前面設定TDSHTTPService元件的HttpPort特性值使用8080。

例如下圖就是筆者使用FireFox存取DataSnap伺服器:

Image7

使用瀏覽器存取DataSnap伺服器的服務

我們也可以使用Delphi/BCB的DataExplorer來測試和存取DataSnap伺服器,例如下圖的三個圖形分別顯示了筆者使用DataExplorer來測試DataSnap伺服器是否支援TCP/IP,HTTP/HTTPS通訊協定,以及存取GetEmployee服務。請注意第3個圖形,它明確的顯示了DataSnap伺服器回傳的是物件型態(Object)。

Image8 Image9 Image10

建立Web用戶端

既然範例DataSnap伺服器支援HTTP/HTTPS通訊協定,因此我們當然可以使用Web應用程式來存取它的服務,例如下圖就是筆者使用VCL For Web建立Web應用程式並且存取DataSnap伺服器的服務:

Image11

這是如何做到的?

由於篇幅的限制,因此讓我們簡單的說明一下好了。當開發人員使用TJSONMarshal類別Marshaling物件時,TJSONMarshal藉由新的Rtti功能取得任何物件的欄位值,再一一的根據欄位型態呼叫TXXXXConverter類別把欄位值轉換為JSON格式,最後再把所有欄位值組合成TJSONObject的型態傳遞出去。至於Unmarshaling則是進行相反的動作。

那麼DataSnap 2010的Marshaling/Unmarshaling能夠傳遞什麼型態的物件呢? 沒有限制嗎? 這要分2個層面回答。

首先回答第一個問題,Marshaling/Unmarshaling能夠傳遞什麼型態的物件呢?答案是任何從TObject繼承下來的物件都可以,因為Marshaling/Unmarshaling中的實作程式碼已經說明了一切:

procedure TTypeMarshaller<TSerial>.MarshalData(Data: TObject);

但另外一個關鍵則是物件的欄位型態,前面筆者已經說過,TXXXXConverter類別,因此如果您要傳遞的物件的欄位型態是特別的型態,或是目前DataSnap 2010的TXXXXConverter類別沒有支援的型態,那麼就不能傳遞,當然要克服這很簡單,因為您可以自己寫一個客製化TXXXXConverter衍生類別,並且註冊給DataSnap 2010就可以了,這屬於進階的功能,以後有機會再討論吧。

如何? DataSnap 2010的Marshaling/Unmarshaling功能是不是很強大又很容易使用? 各位讀者可以和上一篇我們自己傳遞TEmployee物件比較一下。善用DataSnap 2010的Marshaling/Unmarshaling功能可以讓我們輕易的開發出士又輕又快的跨平台分散式應用系統,成功的克服了以往COM/DCOM/COM+無法做到的事情,DataSnap 2010提供了目前最佳的分散式解決方案。

我們下次再見,Have Fun!

September 16

JSON程式設計

上篇文章簡單的介紹了如何使用DataSnap開發基於JSON分散式應用系統,Delphi 藉由原本的Midas/DataSnap和dbExpress的元件提供這個新的分散式計算能力,但使用這些元件都是資料的方式來存取遠端服務,然而我們也可以使用前面介紹的JSON類別再結合DataSnap來實作出存取遠端數值物件(Value Object)或是所謂的DTO(Data Transfer Object)的應用,讓用戶端可以存取遠端的物件。
在下面的範例中我們將展示如何開發使用VO/DTO的DataSnap應用程式伺服器,並且在用戶端擷取其中封裝的資料和物件。這個範例將開發一個可以讓用戶端查詢資料和物件的DataSnap應用程式伺服器,當用戶端查詢物件時,我們將使用TJSONObject封裝物件並且傳遞到用戶端。
X-3-1開發數值物件伺服器
首先建立一個VCL Form應用程式專案,在它的主表單中加入如下的元件:
 

圖7 VO DataSnap應用程式伺服器

接著在專案中建立一個新的程式單元,於其中實作一個簡單的TEmployee類別如下:

unit uEmployee;

interface

uses SysUtils, classes;

type
  TEmployee = class
  private
    { Private declarations }
    FName : string;
    FEMail : string;
    FPhone : string;
  public
    { Public declarations }
    constructor Create(sName  : string = ''; sEMail : string = ''; sPhone : string = '');

    property Name : string read FName write FName;
    property EMail : string read FEmail write FEMail;
    property Phone : string read FPhone write FPhone;
  end;

implementation

{ TEmployee }

constructor TEmployee.Create(sName, sEMail: string; sPhone: string);
begin
  FName := sName;
  FEmail := sEMail;
  FPhone := sPhone;
end;

end.

再於專案中建立一個新的程式單元稱為uEmployeeValueObject,其中定義TEmployeeVO類別如下,請注意TEmployeeVO類別使用了
{$MethodInfo ON}和{$MethodInfo Off}編譯器指令要求編譯器把TEmployeeVO類別的Reflection資訊編譯到執行檔中。

  {$MethodInfo ON}
  TEmployeeVO = class(TComponent)
  private
    { Private declarations }
    FEmployees : TObjectList<TEmployee>;

    function CreateEmployeeJSONObject(employee : TEmployee) : string;
    function CreateEmployeeJSONArray : string;
    function CreateEmployeeJSONObjectJ(employee : TEmployee) : TJSONObject;
  public
    { Public declarations }
    destructor Destroy; override;
    function GetEmployeeJ(const sName : string) : TJSONObject;
    function GetAllEmployeesJ : TJSONArray;
    procedure AddEmployee(const sName : string; const sEMail : string; const sPhone : string);
  end;
  {$MethodInfo Off}

TEmployeeVO宣告了三個方法說明如下:

方法名稱

說明

備註

AddEmployee 讓用戶端呼叫增加員工資訊
GetEmployeeJ 讓用戶端以員工姓名查詢員工物件 請注意GetEmployeeJ回傳TJSONObject型態的結果值,這代表範例應用程式將把員工物件封裝在TJSONObject物件之中並且回傳給用戶端
GetAllEmployeesJ 讓用戶端查詢所有的員工資訊 查詢後端所有員工資訊,請注意GetAllEmployeesJ回傳型態為TJSONArray的結果值,這代表範例應用程式將把所有查詢結果封裝在TJSONArray物件中回傳


下面小節將簡單的說明這些方法的實作。

AddEmployee方法的實作
AddEmployee方法非常的簡單,它從用戶端接受三個字串型態的參數,並且根據它們來建立TEmployee物件,最後再把建立的TEmployee物件加入在宣告為TObjectList<TEmployee>泛型型態的變數FEmployees之中。

procedure TEmployeeVO.AddEmployee(const sName, sEMail, sPhone: string);
var
  employee : TEmployee;
begin
  if (FEmployees = nil) then
    FEmployees := TObjectList<TEmployee>.create(true);
  employee := TEmployee.Create(sName, sEmail, sPhone);
  FEmployees.Add(employee);
end;

GetEmployeeJ方法的實作
GetEmployeeJ方法就非常的有趣了,它展示了如何於DataSnap應用程式伺服器中使用TJSONValue的相關類別,GetEmployeeJ回傳型態為TJSONObject的物件回用戶端。
GetEmployeeJ根據用戶端傳遞來查詢的員工姓名,在Fem ployees中一一的搜尋具有相同名稱的TEmployee物件,找到之涕在013行呼叫CreateEmployeeJSONObjectJ來建立回傳的TJSONObject物件。

001    function TEmployeeVO.GetEmployeeJ(const sName: string): TJSONObject;
002    var
003      ie : TList<uEmployee.TEmployee>.TEnumerator;
004      employee : TEmployee;
005    begin
006      Result := nil;
007      ie := FEmployees.GetEnumerator;
008      while (ie.MoveNext) do
009      begin
010        employee := ie.Current;
011        if (employee.Name = sName) then
012        begin
013          Result := CreateEmployeeJSONObjectJ(employee);
014          break;
015        end;
016      end;
017    end;

CreateEmployeeJSONObjectJ方法根據找到的TEmployee物件來建立TJSONObject物件,它呼叫我們前面學習過的AddPair方法,把每一個TEmployee物件的特性名稱和特性值做為一個JSON的中『名稱/值』配對加入到TJSONObject物件。

function TEmployeeVO.CreateEmployeeJSONObjectJ(employee: TEmployee): TJSONObject;
var
  aJO : TJSONObject;
begin
  aJO := TJSONObject.Create;
  aJO.AddPair(TJSONString.Create('姓名'), TJSONString.Create(employee.Name));
  aJO.AddPair(TJSONString.Create('EMail'), TJSONString.Create(employee.EMail));
  aJO.AddPair(TJSONString.Create('電話'), TJSONString.Create(employee.Phone));
  Result := aJO;
end;

GetAllEmployeesJ方法的實作
GetAllEmployeesJ方法會把所有存在泛型型態變數FEmployees之中的TEmployee物件封裝在TJSONArray物件中回傳給用戶端。
在013行仍然是呼叫CreateEmployeeJSONObjectJ為每一個CreateEmployeeJSONObjectJ建立一個TJSONObject物件,並且加入到回傳的TJSONArray物件中。

001    function TEmployeeVO.GetAllEmployeesJ: TJSONArray;
002    var
003      ie : TList<uEmployee.TEmployee>.TEnumerator;
004      employee : TEmployee;
005      jo : TJSONObject;
006      ja : TJSONArray;
007    begin
008      ie := FEmployees.GetEnumerator;
009      ja := TJSONArray.Create;
010      while (ie.MoveNext) do
011      begin
012        employee := ie.Current;
013        jo := CreateEmployeeJSONObjectJ(employee);
014        ja.AddElement(jo);
015      end;
016      Result := ja;
017    end;

最後不要忘記在主表單中註冊TEmployeeVO類別,如此一來用戶端才能看見並且呼叫TEmployeeVO輸出的方法:

procedure TForm10.FormCreate(Sender: TObject);
begin
  if DSServer1.Started then
    DSServer1.Stop;
  RegisterServers;
  DSServer1.Start;
end;

procedure TForm10.FormDestroy(Sender: TObject);
begin
  DSServer1.Stop;
end;

procedure TForm10.RegisterServers;
begin
  uEmployeeValueObject.RegisterServerClasses(Self, DSServer1);
end;

現在編譯並且執行DataSnap應用程式伺服器,並且準備開發用戶端。

開發範例用戶端
其實這個範例的重點就在於用戶端如何呼叫遠端支援TJSONValue相關類別的方法,這是因為在DataSnap 2009中無法支援TJSONValue相關類別,到了DataSnap 2010才支援。但是在筆者撰寫本章時Delphi 2010的Beta版仍然無法自動產生代表遠端類別的用戶端Proxy類別,因此想要呼叫前面實作的GetEmployeeJ和GetAllEmployeesJ方法,我們必須瞭解如何修改由Delphi產生的DataSnap用戶端類別。
筆者認為當Delphi 2010正式版釋出時,CodeGear應該會把這個問題修正,如果您發現您使用的Delphi已經能夠產生正確的用戶端Proxy類別,那麼就不需要如下所敘述的修改了。
首先建立一個VCL Form應用程式,放入TSQLConnection元件,設定Driver為DataSnap再把connected設定為True(請確定DataSnap應用程式伺服器已經在執行),接著用點選滑鼠右鍵,選擇Generate DataSnap client classes選項,儲存產生的程式單元為uServerProxy.pas,然後搜尋GetEmployeeJ方法,把012行修改為如下:

(註, 筆者已經在Delphi 2010正式版中試過現在沒有問題了, 而筆者之所以沒把上述段落內容刪除是因為這段內容可以讓讀者更瞭解修改下面程式碼的意義, 即使讀者使用Delphi 2010正式版而不需要再修改下面的程式碼, 也可以把uServerProxy.pas開啟, 再看看由Delphi 2010產生的程式碼代表的意義)

001    function TEmployeeVOClient.GetEmployeeJ(sName: string): TJSONObject;
002    begin
003      if FGetEmployeeJCommand = nil then
004      begin
005        FGetEmployeeJCommand := FDBXConnection.CreateCommand;
006        FGetEmployeeJCommand.CommandType := TDBXCommandTypes.DSServerMethod;
007        FGetEmployeeJCommand.Text := 'TEmployeeVO.GetEmployeeJ';
008        FGetEmployeeJCommand.Prepare;
009      end;
010      FGetEmployeeJCommand.Parameters[0].Value.SetWideString(sName);
011      FGetEmployeeJCommand.ExecuteUpdate;
012      Result := FGetEmployeeJCommand.Parameters[1].Value.GetJSONValue as TJSONObject;
013    end;

這是因為遠端GetEmployeeJ方法回傳TJSONObject型態的物件,因此我們需要把012行呼叫遠端方法執行的結果轉變型態為TJSONObject型態。
同樣的,我們也需要修改GetAllEmployeesJ方法,轉變型態為回傳TJSONArray物件,如下所示:

function TEmployeeVOClient.GetAllEmployeesJ: TJSONArray;
begin
  if FGetAllEmployeesJCommand = nil then
  begin
    FGetAllEmployeesJCommand := FDBXConnection.CreateCommand;
    FgetAllEmployeesJCommand.CommandType := TDBXCommandTypes.DSServerMethod;
    FGetAllEmployeesJCommand.Text := 'TEmployeeVO.GetAllEmployeesJ';
    FGetAllEmployeesJCommand.Prepare;
  end;
  FGetAllEmployeesJCommand.ExecuteUpdate;
  Result := FGetAllEmployeesJCommand.Parameters[0].Value.GetJSONValue as TJSONArray;
end;

瞭解了如何以及為什麼需要修改DataSnap用戶端類別之後,我們就可以開始實作呼叫遠端方法的程式碼了。
首先下面的程式碼藉由自動產生的TEmployeeVOClient類別呼叫AddEmployee方法,把用戶端輸入的員工資料加入在遠端的應用程式伺服器中:

procedure TForm11.btnAddEmployeeClick(Sender: TObject);
var
  evo : TEmployeeVOClient;
begin
  evo := TEmployeeVOClient.Create(Self.SQLConnection1.DBXConnection);
  try
    evo.AddEmployee(edtAddName.Text, edtAddEMail.Text, edtAddPhone.Text);
    edtAddName.Text := '';
    edtAddEMail.Text := '';
    edtAddPhone.Text := '';
  finally
    evo.Free;
  end;
end;

下圖是執行範例用戶端應用程式並且點選『增加員工』按鈕以執行上述程式碼的畫面:


圖8 用戶端應用程式呼叫遠端AddEmployee方式加入員工資訊

接著是藉由TEmployeeVOClient呼叫GetEmployeeJ查詢員工資料的實作程式碼:
procedure TForm11.btnQueryEmployeeClick(Sender: TObject);
var
  evo : TEmployeeVOClient;
  jo : TJSONObject;
  employee : TEmployee;
begin
  evo := TEmployeeVOClient.Create(Self.SQLConnection1.DBXConnection);
  try
    jo := evo.GetEmployeeJ(edtQname.Text);
    employee := TEmployee.Create(jo.Get(0).JsonValue.ToString, jo.Get(1).JsonValue.ToString, jo.Get(2).JsonValue.ToString);
    Self.edtQEMail.Text := employee.EMail;
    Self.edtQPhone.Text := employee.Phone;
  finally
    FreeAndNil(employee);
    FreeAndNil(jo);
    evo.Free;
  end;
end;

在上面的程式碼中呼叫GetEmployeeJ取得代表員工的TJSONObject物件,再根據TJSONObject物件中的資訊於用戶端建立TEmployee物件,再顯示員工資訊於表單中,最後不要忘記釋放TEmployee物件,TJSONObject物件和TEmployeeVOClient物件。
下圖是先增加李維這筆員工資料之後,再輸入李維來查詢的畫面:
 

圖9 輸入員工姓名查詢員工資料
點選上圖中的『查詢單一員工』按鈕之後我們的確可以取得遠端員工物件的資訊,如下圖所示:

 
圖10 查詢的結果畫面

但是為什麼上圖中的員工資訊,例如員工的Email有引號包圍呢? 這當然是因為這是JSON封裝字串的規範,而在上面的程式碼中當我們建立TEmployee物件時是直接呼叫ToString方法,如果我們不希望TEmployee物件的特性值有引號包圍,那麼我們可以修改程式碼如下,改呼叫Value方法:
    employee := TEmployee.Create(jo.Get(0).JsonValue.Value, jo.Get(1).JsonValue.Value, jo.Get(2).JsonValue.Value);

那麼就會有如下正確的結果:

 
圖11 查詢的結果畫面
最後讓我們看看如何查詢所有的員工資料。下面的程式碼藉由TEmployeeVOClient呼叫GetAllEmployeesJ取得TJSONArray物件,然後進入for迴圈把其中的每一個元素取出再轉變型態為TJSONObject物件,最後再根據TJSONObject物件一一的建立用戶端的員工物件。

procedure TForm11.btnQueryAllEmployeesClick(Sender: TObject);
var
  evo : TEmployeeVOClient;
  jo : TJSONObject;
  ja : TJSONArray;
  employee : TEmployee;
  iIndex: Integer;
begin
  evo := TEmployeeVOClient.Create(Self.SQLConnection1.DBXConnection);
  try
    ja := evo.GetAllEmployeesJ;
    for iIndex := 0 to ja.Size - 1 do
    begin
      jo := ja.Get(iIndex) as TJSONObject;
      employee := TEmployee.Create(jo.Get(0).JsonValue.ToString, jo.Get(1).JsonValue.ToString, jo.Get(2).JsonValue.ToString);
      Memo1.Lines.Add(employee.Name);
      FreeAndNil(employee);
    end;
  finally
    FreeAndNil(ja);
    FreeAndNil(employee);
    evo.Free;
  end;
end;

下圖是執行此查詢的結果,我們可以看到用戶端的確可以查詢到遠端的所有員工的資訊。

 
圖12 所有員工資料查詢的結果畫面
當然,我們一樣可以修改上面的程式碼如下:
      employee := TEmployee.Create(jo.Get(0).JsonValue.Value, jo.Get(1).JsonValue. Value, jo.Get(2).JsonValue. Value);

那麼我們會看到如下的結果:


圖13 所有員工資料查詢的結果畫面

本章敘述了Delphi VCL框架中支援JSON開發的相關類別,讀者從本章中可以瞭解這些不但是依照JSON規範設計的,而且非常的直覺,好用,只要我們對於JSON規範有基本的掌握就可以順利的使用它們。

此外,新版的DataSnap不但使用JSON做為封裝和傳遞資料的基礎,現在也加入支援使用TJSONValue和衍生類別做為遠端方法的參數和回傳值,如此一來可以讓開發人員撰寫客製化的JSON應用,例如使用VO/DTO設計樣例來傳遞資料和物件,使用這樣的技術,開發人員也可以使用.NETJava或是任何支援JSON的程式語言來開發用戶端應用程式了。

 練完2篇文章的基本功之後, 在下篇文章開發, 我們將介紹如何使用Delphi 2010新的DataSnap Server精靈來大幅簡化開發的工作, 並且會介紹DataSnap強大的Marshaling和unMarshaling的功能,Marshaling和unMarshaling將可以讓我們使用JSON傳遞物件變得非常簡單,使用Marshaling和unMarshaling也可以讓上面傳遞TEmployee物件的程式碼大幅減少. 我們下次再見, Have FUN!



September 11

Delphi 2010 JSON程式設計

從Delphi 2009開始Delphi便開始支援JSON,DataSnap 2009開始使用JSON做為開發分散式應用系統的基礎技術,開始逐漸捨棄使用COM/DCOM/COM+。由於JSON具備跨平台,跨程式語言和跨工具的特性,因此DataSnap在結合JSON和dbExpress之後也逐漸具備了跨平台的基礎。但是Delphi 2009對於JSON的支援只限於使用在DataSnap之中,因此如果開發人員需要進行通用的JSON程式設計,Delphi 2009在這方面仍然顯得不足。

到了Delphi 2010情形開始改變,因為VCL框架開始內建支援JSON的類別,因此Delphi的開發人員就可以利用這些和JSON相關的類別來進行JSON程式設計的工作。本章的重點就是討論VCL框架對於JSON的支援,本章將討論如何使用VCL框架中和JSON相關的類別來進行JSON開發的工作。不過在討論這些類別之前,我們需要先簡單的介紹什麼是JSON。

X-1 JSON是什麼?

簡單的說,JSON是一種資料封裝格式以及資料交換格式,JSON是JavaScript Object Notation的縮寫,它是一種輕量級的資料交換格式,易於一般人閱讀和編寫,同時也易於機器解析和生成。

JSON是基於JavaScript(Standard ECMA-262 3rd Edition - December 1999)的一個子集,JSON採用完全獨立于語言的文字格式,但是也使用了類似於C語言家族的習慣(包括C, C++, C#, Java, JavaScript, Perl, Python等),這些特性使JSON成為理想的資料交換語言。

JSON是由下面的兩個基本架構組成的:

  • “名稱/值”對的集合(A collection of name/value pairs)。在不同的程式語言中,這個架構經常以物件(object),紀錄(record),結構(struct),字典(dictionary),雜湊表(hash table),有鍵列表(keyed list),或者關聯陣列 (associative array)來實作。
  • 值的有序列表(An ordered list of values)。在大部分的程式語言中,這個架構經常使用陣列(array)來實作。

OK,看了上述的定義之後,讀者可能還是覺得模模糊糊,也許讓我們使用實際的範例來說明可以讓讀者更容易瞭解。

假設現在我們有一個DataSnap伺服器,它使用JSON和用戶端進行資料交換的工作,那麼現在用戶端向這個DataSnap伺服器查詢筆者所撰寫的書籍,例如本書『實戰Delphi 2010』,那麼如何把這個資訊以JSON的格式傳遞給用戶端呢?

首先讓我們觀察下圖,在JSON中物件的代表方式是以“{”開始,以“}”結束,另外在前面我們也說明JSON是以『名稱/值』的格式來代表資料,因此從下圖中我們可以看到名稱和值之間是以“:”符號分隔,其中『名稱』是字串格式,而值則是以數值格式來代表:

 



正在更新標籤...

圖1 JSON封裝物件的格式

在JSON規範中上圖的字串規範如下圖所示:




正在更新標籤...


圖2 JSON封裝字串的格式

從上圖中我們可以知道在JSON中字串是以”開始,以”結束,可包含Unicode的字元組。而圖1中的數值格式則如下圖所示:




正在更新標籤...

圖3 JSON封裝數值的格式

從圖3中可知,JSON的數值可以是字串,數字,物件,陣列或是true/false/null值。由於JSON的數值可以是物件或是陣列,而物件又可以包含字串:數值配對,因此JSON規範可以封裝任何複雜的資料。

有了上述對於JSON基本的瞭解之後,我們就可以使用如下的格式從DataSnap伺服器傳遞資料到用戶端:

{“書名”:”實戰Delphi 2010”}

從這個格式中我們可以知道這是用JSON中封裝物件的規則,把『字串:數值』配對包含在{和}符號之中。

如果我們希望也傳遞作者的資訊,那麼可以使用逗號分離每一個『字串:數值』配對,如下所示,讀者也可以回頭再參考圖1就可以發現這是圖1的規則。

{“書名”:”實戰Delphi 2010”,“作者”:”李維”}

如果現在再把書籍出版年份也傳遞,那麼就可以使用下面的格式:

{"書名":"實戰Delphi 2010","作者":"李維","出版年份":2010}

請注意的是,出版年份的數值是2010這個數字,因為如圖3所示數值可以是數字(number),而在JSON中數字的定義如下:



正在更新標籤...

 

圖4 JSON封裝數字的格式

從圖4我們可以知道,在JSON中數字可以是整數或是浮點數。

{"書名":"實戰Delphi 2010","作者":"李維","出版年份":2010}

因此如果我們再傳遞書籍價格,那麼可以使用如下的格式:

{"書名":"實戰Delphi 2010","作者":"李維","出版年份":2010,"價格":45.95}

最後如果我們再加入書籍已出版否資訊,那麼可以使用如下的格式:

{"書名":"實戰Delphi 2010","作者":"李維","出版年份":2010,"價格":45.95,"已出版否":false}

從上面的範例中我們可以看到JSON規範如何封裝各種不同型態的資料。那麼如果我們需要一次傳遞多筆資料到用戶端的話,又要如何封裝呢?這可以使用JSON中陣列的規範。

圖5是JSON陣列的封裝規則,陣列“[”開始,以“]”結束,而陣列中的每一個元素都是數值,每一個元素使用逗號分隔。回頭參考圖3 JSON封裝數值的格式,由於數值可以是物件,因此我們如果需要一次傳遞多筆資料到用戶端,那麼就可以使用JSON的陣列來封裝。




正在更新標籤...

圖5 JSON封裝陣列的格式

例如下面就是使用JSON的陣列封裝性三個代表書籍的物件,陣列中的每一個元素數值是物件,每一個元素數值使用逗號分隔:

[{“書名”:”實戰Delphi 2010”},{“書名”:”Inside VCL”},{“書名”:”Borland傳奇”}]

如何? 一旦瞭解了JSON封裝資料的規則之後讀者是不是覺得使用JSON非常的簡潔呢? 正由於JSON在封裝資料的規則比較簡單,因此在解析JSON資料時速度也比較快,JSON的簡潔規則讓JSON擁有較少的資料流量和較快的處理速度,因此讓JSON在Web應用方面佔有優勢,也愈來愈受歡迎,這也是DataSnap現在選擇使用JSON做為基礎技術的原因。

在離開本小節之前,讓我們比較一下使用XML和JSON在封裝和交換資料方面的差異。下面是使用XML封裝一個員工的資訊:

<?xml version="1.0" encoding="utf-8"?>

<user>

<name>李大明</name>

<password>123456</password>

<department>R&D</department>

<gender>男</gender>

<age>26</age>

</user>

如果我們使用JSON來封裝的話,那麼就如下所示:

{

"name":"李大明",

"password":"123456",

"department":"R&D",

"gender":"男",

"age":"26"

}

我們可以看到JSON使用了較XML少的資料量來封裝相同的資訊,此JSON在資料交換方面擁有比較多的優勢。

讀者可以參考www.json.org以瞭解更多有關JSON的資訊。

在對於JSON有了基本的瞭解之後,更重要的是我們需要知道如何在Delphi中進行JSON的程式設計。

X-2 VCL框架中支援JSON的類別

Delphi在2009開始使用JSON做為DataSnap 2009封裝和傳遞資料的格式,但是Delphi 2009對於JSON的支援和DataSnap的功能撰寫的非常緊密,開發人員除了能夠在DataSnap 2009中使用JSON之外,並不容易使用Delphi 2009來開發其他應用型態的JSON應用程式。到了Delphi 2010這個現象獲得了大幅的改善,Delphi 2010提供了許多通用的JSON相關類別,開發人員可以使用這些通用的JSON相關類別開發任何型態的JSON應用程式而不只限於DataSnap應用程式。

Delphi 2010在新的DBXJSON.pas程式單元中提供了這些JSON相關的類別,如果讀者仔細觀察前面JSON規則圖的話,就可以發覺如果我們需要使用類別來實作支援JSON的規範,那麼我們我們至少需要下面的三個類別:

類別                 說明

JSONValue 用來代表和實作圖3的實體和關係

JSONPair         用來代表JSON規範中的『名稱/值』配對實體和關係

JSONObject 用來代表和實作圖1的實體和關係

JSONString 用來代表和實作圖2的實體和關係

JSONArray 用來代表和實作圖5的實體和關係

JSONNumber 用來代表和實作圖4的實體和關係

當然在上述的基礎類別中還存在繼承的關係,例如由於JSON規範中數值可以代表字串,物件,陣列等實體,因此上面的JSONObject,JSONString等類別可以從JSONValue類別繼承下來。

Delphi 2010中支援JSON等相關類別就是使用這個觀念實作出來的,因此只要讀者瞭解了前面解釋JSON規範的觀念,就可以非常直覺的瞭解這些實作類別。在下面的表格中整理了這些類別,並且提供了簡單的敘述:

類別                 說明

TJSONAncestor 抽象類別,是所有JSON相關類別的根類別

TJSONPair 實作JSON規範中『名稱/值』配對的類別

TJSONValue 實作JSON規範中數值的類別,TJSONValue是TJSONAncestor的繼承類別

TJSONTrue 實作JSON規範中代表True的類別,從TJSONValue繼承下來

TJSONString 實作JSON規範中代表字串的類別,從TJSONValue繼承下來

TJSONNumber 實作JSON規範中代表數值的類別,它從TJSONString繼承下來,因為JSON使用文字格式傳遞封裝和傳遞資料,因此所有數值都將轉換為文字字串型態傳遞,到達目的地之後再反轉回數值

TJSONObject 實作JSON規範中代表物件的類別,從TJSONValue繼承下來

TJSONNull 實作JSON規範中代表Null的類別,從TJSONValue繼承下來

TJSONFalse 實作JSON規範中代表False的類別,從TJSONValue繼承下來

下圖是這些類別之間的繼承和關連關係圖,請讀者對照下圖和以前圖1到圖5的JSON規範,就可以瞭解這些類別的實作是完全從JSON規範中設計出來的,簡單又符合直覺:

 

圖6 Delphi中支援JSON的類別架構

接下來讓我們簡單的說明這些類別提供的服務,如此一來讀者就可以瞭解如何在程式碼中使用它們。

X-2-1 TJSONAncestor類別

TJSONAncestor類別是所有JSON相關類別的根類別,它主要定義了三個虛擬方法讓它的衍生類別來複載實作,下面的表格敘述了這些虛擬方法:

虛擬方法                                                                 說明

function ToString: UnicodeString; virtual;                 ToString虛擬方法會把JSON的內容轉換為文字字串形式回傳,TJSONAncestor的衍生類別需要實作這個虛擬方法,例如TJSONObject類別會實作這個虛擬方法並且把物件內容轉換為字串的形態回傳

function EstimatedByteSize: Integer; virtual; abstract; EstimatedByteSize虛擬方法回傳JSON物件預估的位元組大小,TJSONAncestor的衍生類別需要實作這個虛擬方法,每一個衍生類別都會回傳它本身需要佔據位元組的大小。EstimatedByteSize虛擬方法的目的是在封裝和傳遞JSON物件時程式碼能夠配置足夠的記憶體大小之用。

function ToBytes(const Data: TBytes; const Offset: Integer): Integer; virtual; abstract; ToBytes虛擬方法能夠把JSON物件以位元組的形式回傳,TJSONAncestor的衍生類別需要實作這個虛擬方法,每一個衍生類別都會在這個複載的虛擬方法中把自己轉換為位元組形態。

TJSONAncestor本身是一個抽象類別,因此開發人員並不應該直接在程式碼中使用它,而是應該使用下面介紹的衍生類別。

X-2-2 TJSONValue類別

TJSONValue類別本身也是一個抽象類別,它是直接從TJSONAncestor繼承下來的,TJSONValue類別只是做為一個Placeholder,它主要的目的是代表圖3中JSON規範value的概念,因此它的定義非常簡單,如下所示:

  TJSONValue = class abstract(TJSONAncestor)

  end;

稍後介紹的許多類別都是從TJSONValue繼承下來的,幾乎和圖3顯示的規範架構一樣。

X-2-3 TJSONPair類別

TJSONPair類別是實作JSON規範中『名稱/值』配對的類別,它從TJSONAncestor直接繼承下來,由於TJSONPair是實作『名稱/值』配對,因此它提供了兩個最重要的特性,JsonString和JsonValue:

    property JsonString: TJSONString read GetJsonString write SetJsonString;

    property JsonValue: TJSONValue read GetJsonValue write SetJsonValue;

JSonString是代表『名稱/值』配對中名稱的特性,而JsonValue則代表其中值的特性。請注意的是JsonValue的型態是TJSONValue,而根據前面表格所敘述,TJSONValue可以代表任何從TJSONValue繼承下來的類別,因此JsonValue可以代表TJSONString,TJSONNumber或是TJSONObject等。

那麼如何建立TJSONPair呢? TJSONPair定義了四個複載的建構函式,其中最重要的三個如下所示:

    constructor Create(const Str: TJSONString; const Value: TJSONValue); overload;

    constructor Create(const Str: UnicodeString; const Value: TJSONValue); overload;

    constructor Create(const Str: UnicodeString; const Value: UnicodeString); overload;

上面第一個建構函式可以藉由TJSONString和TJSONValue物件來建立TJSONPair物件,第二個建構函式可以使用Unicode字串和TJSONValue物件來建立TJSONPair物件,至於第3個建構函式則是使用兩個Unicode字串來建立TJSONPair物件。

例如,如果我們要建立如下的JSON『名稱/值』配對:

“書名”:”實戰Delphi 2010”

那麼我們可以使用如下的程式碼:

var

  jp : TJSONPair;

begin

  jp := TJSONPair.Create(TJSONString.Create('書名'), TJSONString.Create('實戰Delphi 2010'));

  try

    Memo1.Lines.Add(jp.ToString);

  finally

    jp.Free;

  end;

上面的程式碼使用了第一個建構函式建立TJSONPair物件,TJSONPair類別的ToString方法可以把JSON物件之中的內容以文字字串形式回傳,最後釋放TJSONPair物件時,TJSONPair物件會自動釋放傳入建構函式之中的兩個TJSONString物件。

當然您也可以使用第二個建構函式來建立TJSONPair物件,如下所示:

  jp := TJSONPair.Create('書名', TJSONString.Create('實戰Delphi 2010'));

或是使用第3個建構函式,因為在這個範例中『名稱/值』配對中的名稱和值都是字串:

  jp := TJSONPair.Create('書名', '實戰Delphi 2010');

當然,如果您需要建立如下的JSON『名稱/值』配對:

"價格":45.95

由於值是數字,因此我們只能使用第一個或是第二個建構函式來建立:

  jp := TJSONPair.Create(TJSONString.Create('價格'), TJSONNumber.Create(45.95));

或是:

  jp := TJSONPair.Create('價格', TJSONNumber.Create(45.95));

一旦建立了TJSONPair物件,我們也可以藉由存取它的JsonString和JsonValue特性來存取『名稱/值』配對中的名稱或是值了,例如:

    Memo1.Lines.Add('JSONString : ' + jp.JsonString.ToString);

    Memo1.Lines.Add('JSONValue : ' + jp.JsonValue.ToString);

X-2-4 TJSONString類別

TJSONString是從TJSONValue類別繼承下來,它是使用來代表Unicode字串的資料,它可以是『名稱/值』配對中的名稱或是值或是兩者。

TJSONString最重要的方法應該是它的建構函式了,它接受一個Unicode字串做為參數:

    constructor Create(const Value: UnicodeString); overload;

在Unicode字串使用建立TJSONString物件之後,如果開發人員需要加入額外的字元,那麼可以呼叫AddChar虛擬程序,AddChar會在原本的Unicode字串之後加入參數Ch的字元內容。

    procedure AddChar(const Ch: WideChar); virtual;

如果開發人員需要TJSONString物件中字串的內容值,可以呼叫ToString:

    function ToString: UnicodeString; override;

如果開發人員只是需要TJSONString物件中字串的內容值,而不需要額外的開始”符號以及結束的”符號,那麼可以呼叫TJSONString的Value函式

    function Value: UnicodeString; override;

例如下面的程式碼:

var

  js : TJSONString;

begin

  js := TJSONString.Create('實戰Delphi 2010');

  try

    Memo1.Lines.Add('TJSONString.ToString : '+ js.ToString);

    Memo1.Lines.Add('TJSONString.Value : '+ js.Value);

  finally

    js.Free;

  end;

下面是呼叫ToString和Value的差異:

TJSONString.ToString : "實戰Delphi 2010"

TJSONString.Value : 實戰Delphi 2010

X-2-5 TJSONObject類別

TJSONObject類別從TJSONValue直接繼承下來,它代表JSON規範中封裝物件的類別。要建立TJSONObject物件非常簡單,只需要呼叫它的建構函式即可:

    constructor Create;

那麼如果我們需要建立如下的JSON物件內容,那麼應該如何做呢?

{“書名”:”實戰Delphi 2010”}

請注意上面的結構,其實是在JSON物件之中包含一個TJSONPair物件,因此我們只需要執行下面的步驟即可:

  • 建立TJSONObject物件
  • 建立TJSONPair物件
  • 把TJSONPair物件加入到TJSONObject物件

要把TJSONPair物件或是JSON『名稱/值』配對加入到TJSONObject物件中,我們可以使用TJSONObject類別中下面兩個複載的AddPair方法:

    procedure AddPair(const Pair: TJSONPair); overload; virtual;

    procedure AddPair(const Str: TJSONString; const Val: TJSONValue); overload; virtual;

因此如果我們需要建立如下內容的TJSONObject物件:

{"書名":"實戰Delphi 2010","出版年份":2010}

那麼可以使用如下的程式碼:

var

  jo : TJSONObject;

  jp : TJSONPair;

begin

  jo := TJSONObject.Create;

  try

    jp := TJSONPair.Create('書名', '實戰Delphi 2010');

    jo.AddPair(jp);

    jo.AddPair(TJSONString.Create('出版年份'), TJSONNumber.Create(2010));

    Memo1.Lines.Add(jo.ToString);

  finally

    jo.Free;

  end;

同樣的TJSONObject類別的ToString方法可以把其中的內容以文字字串型態回傳:

    function ToString: UnicodeString; override;

X-2-5 TJSONNumber類別

TJSONNumber類別是從TJSONString類別繼承下來的,這是因為在JSON規範中整數和浮點數都必須使用字串形式來代表,由於TJSONString已經提供了使用字串形式來代表JSON內容的功能,因此TJSONNumber只需要從它繼承下來並且把整數和浮點數轉換為文字字串型態即可。

在前面我們看過多次建立TJSONNumber物件的範例了,它的建構函式接受一個double值的參數:

    constructor Create(const Value: Double); overload;

一旦建立了TJSONNumber物件,呼叫它的ToString方法即可取得其內容。

在離開本小節之前再讓我們看看三個剩下簡單的類別,它們是TJSONTrue,TJSONFalse和TJSONNull。這三個類別分別實作圖3中的true,false和null三個JSON數值。例如我們如果需要下面的JSON內容:

{"書名":"實戰Delphi 2010","出版否":false, “撰寫中”:true, “出版商”:null}

那麼我們使用下面的程式碼即可:

var

  jo : TJSONObject;

  jp: TJSONPair;

begin

  jo := TJSONObject.Create;

  try

    jp := TJSONPair.Create('書名', '實戰Delphi 2010');

    jo.AddPair(jp);

    jp := TJSONPair.Create('書名', TJSONFalse.Create);

    jo.AddPair(jp);

    jp := TJSONPair.Create('撰寫中', TJSONTrue.Create);

    jo.AddPair(jp);

    jp := TJSONPair.Create('出版商', TJSONNull.Create);

    jo.AddPair(jp);

    Memo1.Lines.Add(jo.ToString);

  finally

    jo.Free;

  end;

在讀者瞭解了如何使用這些JSON相關的類別之後,讓我們看看如何使用它們在分散式應用系統之中。




Delphi 2010產品技術發表會投影片和範例檔!

本來昨天(星期四)就應該把2個壓縮檔放上來, 但由於昨天我一直無法連上我的部落格, 到今天連上我的部落格才發現MS的部落格不能上傳檔案, 只能上傳相片, 哇勒, 而以前CSDN提供給我的地方也不能用了, 沒想到現在連個棲身之地都沒了, 因此我得再找其他地方, 等我找到地方上傳之後再公布下載位址吧, 在這裡向各位說一聲抱歉. 


OK, 我已經把Delphi 2010產品技術發表會投影片和範例檔給了興德請興德放在網上讓各位下載, 各位可以在下面的地址找到(噢, 我現在才發現他們把我的英文名字也拼錯了, 哇勒是Gordon不是Golden啊):

http://www.sinter.com.tw/codegear/codegear_technique.html#golden_book

 
和Delphi相關的資訊
C++Builder相關技術
和JBuilder相關的資訊
No list items have been added yet.
結合ECO和VCL For Web開發的相關資訊
No list items have been added yet.
好玩又好用的RoR/XXXX相關資訊
No list items have been added yet.
Photo 1 of 8
More albums (1)