製作WPF聯機飛行棋的失敗體驗

來源:互聯網
上載者:User

      飛行棋作為幼時的娛樂項目在我的記憶裡印象是相當深刻的,用編碼實現它也一直是我自己的目標。WPF有著映像編碼的舒適體驗,自然成為我的首選;伴隨著WPF的Binding,一種新的模式也應運而生——MVVM(Model-View-ViewModel),使得頁面和邏輯更好的分離。可這次的體驗對我而言不管從技術到思想都深深的受到了打擊。

         

 

起因:MVP模式的討論

 

當然讓我衝動的在沒有什麼準備的前提下做這款遊戲還是由於以下對話:

 

某人:你們現在還在用MVP模式嗎?

  我:我們現在用MVVM。

 

某人:MVVM?

  我:WPF綁定功能很強大,可以實現View 和 Model的完全分離。

 

某人:用MVP也可以分離,Presenter作為兩者的媒介就可以了。

   我:你們現在的做法是不是從Service 上擷取一些DTO,然後通過Presenter為控制項賦值,在Presenter註冊IView上的控制項事件(下面會說到只放方法、屬性和事件),在事件裡回調Service。

 

某人:沒錯,有什麼問題嗎?

   我:假設說對象是Employee,上面有一些常用的屬性Name,Id,Address外,或許需要還有個DoWork方法,而且Employee可能是個抽象類別,下面還有各種分工,每個子類的DoWork方法是不同的。按照物件導向的思想是不是DoWork是Employee對象上的。而你們現在的做法是Service 上有個DoWork方法然後把Emplyee對象傳進去,這樣你們傳遞的對象沒有行為只有屬性,好聽點叫做貧血模式,實際換個C語言時候的結構體也一樣;對象上的函數才叫方法,而Service只是函數的集合而已,現在的說法是面向服務編程,可骨子裡卻是面向過程。

 

某人:是的,對象上應該有行為才是物件導向編程。可主流的方法都是這樣,WebSerivce,WCF;即使你直接連接的是資料庫也需要一層轉換你總不會在方法裡直接下SQL吧,即使下了SQL也是面向過程了,因為資料庫不是物件導向的。

  我:嗯.. 我們也會用一些面向過程的操作,可我們盡量應該避免面向過程的行為。

 

某人:那麼就實現對象行為而言,把從Service擷取的DTO在用戶端轉換成一個對象,在對象的方法中封裝調用Service的服務,在控制項的事件裡不直接調用Service而是調用那個對象的方法不就可以了,我們現在就是這麼做的。

  我:這樣確實可以,實際上即便是WPF也需要註冊一些事件來滿足需求。但假如介面上需要顯示一個人員的姓名,當你修改姓名的時候是不是要在IView上把控制項列出,在Presenter註冊一個類似TextChanged事件,一個還可以,假如還有電話、住址等雜七雜八的一些東西,是不是Presenter單單註冊事件就變得臃腫了,而且你IView上需要曝露具體控制項,如果IView上不是曝露具體控制項,而是些屬性和方法時,隨著控制項操作的繁多IView會慢慢變的混亂不堪,畢竟賦值,值變化,操作變化都要對應的加上方法或屬性,那時你就會想從View上直接調用Presenter的方法,可MVP的思想是View 不知道Presenter,Presenter 可以操縱View,要不然就變成了MVC了。

 

某人:這對於IView的維護確實不容易,假如去掉IView直接讓Presenter操縱具體的View呢?

   我:定義IView還有個意圖,也就剛才說的只在上面添加屬性和方法,這樣做的目的是為了介面的變化,WEB能用,WINFROM也能用。一般常用的方法是View放在一個項目,Presenter放在一個項目,IView在另外一個項目,View中通過IOC容器註冊IView,當Presenter啟動IView時,IOC容器自動的會去找到對應的View來做執行個體,這樣Presenter和View的引用是分離的。也就是說IView變成View的話Presenter和View就要放在一起,最起碼也是要Presenter引用View的項目,雖然也是MVP,不過感覺弱些。

 

某人:我們現在就是把Presenter和View放在一起的,甚至是放在一個檔案夾中,Presenter負責的是View上資料的呈現變換,就是把View上的對應資料變化通過Presenter賦值到用戶端的對象中——也就是DTO轉出的對象。我們把這些對象放在另一個項目中,你該不會認為我們會把一些邏輯放在Presenter中處理,畢竟介面的變換隻是資料的表現,換個UI而已對象還是在的,對象也不知道具體的Presenter,這樣也算作一種分離吧,再者你剛才說的IView上放屬性和方法,如Button的Click是不是又要加個事件,事件本身就是和UI有聯絡了,除非在IView層上再添加些對象來傳遞資訊,可這樣感覺是為了分層而分層了。

   我:那你們的Presenter可以從多個對象上擷取資料嗎?比如View上的資料有一部份來自A對象,一部分來自B對象,而A和B屬於並列關係。

 

某人:目前不能,我們一個Presenter只對應一個對象,如果發生這種情況,我們會把Presenter切的更細。

   我:也就是說A和B對象各對應一個View,那麼A View 和B  View 的容器C View,對應的也是A對象和B對象的容器C對象,也可以說C對象上有A和B屬性?

 

某人:A和B對象是各對應一個View,你說的這種C View的是一種做法,可有時候A對象和B對象作為C 對象的屬性會使對象臃腫;假設A對象的產生必定會產生一個View,那麼介面設計者是應該知道這個View停靠在哪的,比如該View同你所說是停在C View 容器中那麼在C容器中會有地方留給它,我們的做法是寫個CustomerPanel類似SCSF中的容器,在上面寫上需要放的View的RegionName,通過一些容器再做個橋接應該就可以達到這個效果了。

   我:你們能達到一個View多個地方麼,比如我修改的資訊的View是在一個地方,新增資訊又是在彈出的視窗中。

 

某人:可以,因為資料不一樣,建立對象首先建立出對應的Presenter,Presenter根據對象的一些資訊來決定View的RegionName,一個View有多個RegionName,當然一個RegionName只能出現一個地方。

   我:那對象中資料的變化怎麼讓Presenter知道呢?比如你改變了A屬性但B屬性在A變化的邏輯中也發生了變化,而B也是在View上要顯示的一個欄位,你怎麼偵測B屬性的變化,寫個事件給Presenter註冊?

 

某人:這樣做會有什麼問題嗎?

   我:如果是普通事件的話,就會有強引用問題,當你View關閉了,可對象還不需要銷毀,這樣對象上還是儲存著Presenter的引用,這會造成記憶體泄露。

 

某人:實際上我們的對象會繼承自一個基類,基類上做了個弱事件機制,就是弱引用住委託,把弱引用放在一個List裡面,如果弱引用不為空白也就是還存在則引發其中的委託,Presenter會註冊這個弱事件,得到他的通知,很像Winform中的INotifypropertychanged機制。

   我:WPF也是靠這個機製做的Binding,不過比Winform強些,另外他是通過WeakEventManager來管理註冊的事件,我感覺你們View和Presenter的關係只是一個不知道業務對象,一個知道。

 

某人:這樣不是可以更好的讓前端設計師發揮麼,業務對象畢竟涉及到商務邏輯,前端應該主要側重是UI,UE的設計。

   我:你說的有道理,你們的設計確實可以滿足大部分需求,可前端設計師也是需要知道一些商務邏輯的,要不然如何設計出符合使用者操作的介面。你的一些類似姓名,地址之類的文本賦值其實直接知道用戶端對象也未嘗不可,這樣可以減少Presenter的工作量,畢竟對於文本賦值,文本改變這樣的簡單操作Presenter的設計顯的太庸俗化,而且對於行為的操作,比如點擊Button大體會產生什麼效果,介面設計師應該也是清楚的。

 

某人:我們的原則是有關介面變化的操作盡量在View層操作,如Button點擊會產生提示框,這個提示框的樣式、位置是View層控制的,內容是Presenter賦予的。不過就同你所說Presenter的工作量大、繁雜,需要完全瞭解View和用戶端邏輯層,但是Presenter不就是做兩者的承接麼。MVVM有更好的體驗?

   我:MVVM 是由WPF的控制項機制決定的,它能夠比較輕易的實現Bindng,和Winform的那個Binding差不多,前端的TextBox裡的值改變了,後端的類中對應的屬性也跟著變化,這樣就省去了Presenter的麻煩賦值,對於控制項的事件也就是行為WPF提供了Command機制,這樣你每個View真的只對應一個對象,有行為有屬性,有血有肉。不過缺點是介面需要用戶端對象,我們把這個用戶端對象稱為ViewModel,為了更好的實現分離和互動,可以把ViewModel做個介面,View只知道介面,這樣View要呈現哪些更清晰些。

 

某人:好像有點抽象,WPF特有的?有什麼例子嗎?

   我:我正好準備寫個簡單的遊戲,可以到時候讓你開開眼界,實現的代碼絕對優雅。

 

經過:錯誤的設計

 

      會出現錯誤設計的大體原因一般有這麼幾個:技術痛點沒有抓住,業務需求沒有搞清.當然對我而言還有很多原因,如9月開學了,還有一堆考試和重考,白天又要上班,精力不夠,所以想把它快點解決。

 

錯誤一:

      由於沒有做過網路互動程式的經曆,我開始是這麼想的,通訊方式使用WCF,可糟糕的是我沒有具體的學過WCF,仗著自己對網路通訊的一知半解,認為可以怎麼簡單怎麼來,用TCP,UDP無所謂,能達到效果就成,以致於到本程式完成差不多前一周才開始瞭解學習WCF。

      WCF可以使用svcutil 工具組建代理程式類,也可以把服務端的契約自動轉成用戶端類,只要在svcutil 的串連後加上/edb 便可以產生的用戶端類帶有INotifiyPropertyChanged介面,並自動實現一個RaisePropertyChanged方法,在屬性的賦值中調用RaisePropertyChanged。這其實應該是方便開發人員的,一般盡量不要去修改這個產生的類,因為當伺服器端改變時可以更方便的自動產生,要不然常常修改這個"大塊頭"也是件麻煩的事;但堅持這個做法也讓我比較累,比如我本來想Room中放個Dictionary來當座位然後在其中放Player(玩家類),由於不考慮參觀人數,所以我就可以把Dictionary設成4個,然後用個for迴圈便可以判斷座位上有沒有人(不用列表List的原因是線程間安全),可如果Seats[0] =Player UI是不會重新整理的,因為沒有引發PropertyChangedEventHandler,Seats是屬性除非為Seats = Seats 的動作,不過即使這樣,自動產生代碼是這樣的:

 

public System.Collections.Generic.Dictionary<int, ModelModule.Player> Seats{    get    {        return this.SeatsField;    }    set    {        if ((object.ReferenceEquals(this.SeatsField, value) != true))        {            this.SeatsField = value;            this.RaisePropertyChanged("Seats");        }    }}

 

大家看出來,自己把自己賦值給自己的這條路也行不通,然後的我不得不在partial class中寫

 

/// <summary>/// player不為空白把座位上指定玩家否則移除玩家/// </summary>/// <param name="seatNo">座位號</param>/// <param name="player">玩家</param>public void SetSeat(int seatNo, IPlayer player){    Seats[seatNo] = (Player)player;    if (player == default(Player))    {        RemovePlane(seatNo);    }    RaisePropertyChanged("Seats");    Application.Current.Dispatcher.BeginInvoke((Action)CommandManager.InvalidateRequerySuggested);}
 

如果再設計,我一定要加個Seat的概念,Player中對應的屬性不是Room而是Seat,Seat知道房間號和坐席號。我現在是Player有個屬性是Room還有個屬性是SeatId。這個設計是我現在最耿耿於懷的。

 

錯誤二:

      對WPF信心太高,認為WPF可以實現更快速的快發,實際上套用MVVM需要自訂大批量的控制項,需要大量的AttachBehaiver類,需要更高設計思想來分離ViewModel。其中設計最爛一個完全可以讓大家鄙視我的做法是,居然不是在讓服務端判斷戰鬥的勝負,而是讓用戶端告知的,用戶端自己得知輸贏。為什麼會這樣的設計的一個原因是,我最初死都不想讓用戶端和伺服器端各用一遍飛行邏輯,要不讓服務端產生傳給用戶端需要走的格子,用戶端完全全只是UI的呈現,當時劈頭劈腦的也做好了,無非是把座標轉成路徑嘛,用MatrixAnimationUsingPath跑下就好了。可問題又來了,一架飛機飛越另一架飛機是要爆炸的,這個時機點要準確,只告訴座標無疑太沒用,也就是說還要傳遞在哪個格子上消滅了哪幾個飛機,用主要畫面格的方式實現了下,感覺太蠢,搞了好久,最後因為時間花費的是在太久,就用了現在的方法,用戶端接受伺服器的色子數跑,由於在類中跑一個格子太快,我只好用了AutoResetEvent來等,這應該是我程式生涯中最不爽的一件事。重新再來的話,我一定讓服務端執行飛行邏輯,用戶端也執行飛行邏輯(只接受服務端所給的色子數,節省傳輸資源),但用戶端的飛行邏輯只為產生飛行路線,在另一個邏輯中判斷飛機是否碰撞到另外的飛機,飛行動畫效果也不用什麼內建的什麼Storyboard在CompositionTarget.Rendering中用RenderTransform屬性就好了。因為用AnimationTimeline類,你的一些呈現便會捕捉不到,註冊他的CurrentTimeInvalidated事件又太費,我們不能控制AnimationTimeline的時鐘,本來打算飛機飛過的時候有個尾氣效果,可花了很大的功還是不能達到理想的效果。

 

遊戲整體設計思路及做法

      大廳的設計並沒像市面上的遊戲大廳一樣有多個區,如新手區、高手區。因為本意只是做個Demo,能在區域網路環境下跑就可以了,所以玩家開啟畫面只會看到一個大廳。我把大廳取名為Area,由於Area只需一個執行個體,在服務端以Sington方式運行。

      Area中有Room(房間),預設數目為50,房間數目應該是Area建立後便不會改變,我就把它放在了Area的建構函式中初始化,Player(玩家)在用戶端執行個體時自動建立一個Id(Guid),調用服務端的Enter方法進入大廳,並得到伺服器上的房間,房間的結構中便包含了在房間中的玩家。

      為什麼不返回進入大廳的全部的人員,而只返回房間?

      當時的考慮點是讓玩家看到進入大廳的人意義不大,這個簡單的遊戲並不包括廳內聊天功能。只需讓玩家知道哪幾個房間內坐了人,有幾個人舉了手,該房間是否開始遊戲就足夠了。

      服務端的Enter方法,不僅會儲存玩家的基本資料,還會儲存玩家的通訊管道(OperationContext.Current.GetCallbackChannel<IPlaneChessCallback>()),我想對於安全的話,每次用戶端放送請求都應該檢查該通訊管道是否一致。

      用戶端調用伺服器端方法EnterRoom進入房間,這時服務端應該判別該座位是否已有人,如果有人則拋錯(可憐的我還不懂如何搞WCF的異常資訊),進入房間應該通知進入大廳的所有人,通知所有人的操作還包括舉手,退出房間,房間開始遊戲,房間結束遊戲。

      當進入房間的玩家都舉了手(房間中的玩家大於一),遊戲便開始。遊戲開始後的輪圈,系統隨即骰出的色子數只需要通知房間內的人。

      用戶端選中飛機給服務端骰色子,服務端骰出色子通知房間內的人,如果在一定時間內(預設20秒)沒有動作,用戶端會自己重選中一架飛機提交服務端。

      當用戶端發現遊戲完成,則通知服務端,服務端通知進入大廳的全部人該房間遊戲已結束。

      當服務端發送給用戶端資訊失敗是,先判斷該玩家是否在房間中,如果有房間,則先退出房間,然後再從系統的人員列表中刪除。

 

項目結構設計

      

       服務端:Host為啟動程式,Service為實體類和服務類

       用戶端:WPFClient為啟動程式

                  ControlLibrary為WPF控制項陳列庫

                  Common為IOC和讀取App.cofig的東西

                  ViewModule為介面

                  ModelModule為ViewModel

        值得注意的是在WPFClient中我並沒有添加ViewModule和ModelModule的引用

      

       在設定檔中

<modules>  <module assemblyFile="ViewModule.dll" moduleType="ViewModule.Module, ViewModule" moduleName="ViewModule">  </module>  <module assemblyFile="ModelModule.dll" moduleType="ModelModule.Module, ModelModule" moduleName="ModelModule">  </module></modules>

        這是學習Prism架構的模組做法,我針對項目自己做了些改進,讀出Assembly後,看這個程式集中是否有繼承於IModule的類,有的話執行個體那個類並調用IModule介面中的Register方法來註冊相應的類到IOC容器中。

         可是ModelModule和ViewModule沒有引用到WPFClient中,如果要讀取它們的Assembly還是要把dll還是拷貝到WPFClient對應的程式目錄下。需要加以下命令:

          xcopy "$(TargetDir)*.*" "$(SolutionDir)WPFClient\bin\$(ConfigurationName)\" /Y

為何需要勞師動眾的這麼搞?意義何在?

這是為了模組組建化,我原本想把房間內的聊天做成另一個項目,然後可以作為外掛程式構建進去,也就是說在原來的View上留好一個地方,我的聊天View就可以通過一定的容器放進去,在Prism中的做法便是留一個Region,然後在View上寫上RegionName,其實放的RegionName的容器不一定是單容器,可以是Menu,Listbox,Canvas,StackPanel這樣的多容器,自然地你要為這樣的多容器寫Adapter。

 

結果:反思

      首先在沒有十足的把握下少說大話,即使有一定把握也要低調

      沒有按照軟體工程的思想去設計,整個流程應該是先設計類,再設計介面,然後修正類,  並且每一步要列出詳細的時間表,允許用最濫的方法先完成,時間允許再修改。在某一點花費的時間太久往往會造成以後的設計都沒有經過更好的思考,所以除非已經完成或是錯誤實在太大才允許修正以前的不足之處,最起碼先拿一個版本出來,這樣做的時候心裡有底,考慮問題也更全面些。當然在類設計完畢後測試也要跟上。記得剛學編程的時候有本網路上流傳的C++ 箴言,現在越來越感覺他說的是真理。C  不是C ++ ,C++ 也不是C#,可以說有機結合,也可以說完全不同——語言的特性使得設計思路完全不同。

     有的地方太技術而技術了,比如Storyboard完全可以捨棄的。今天做公交的時候看到一個動畫大片的廣告,其中有一句話印象深刻:動畫片是以故事為主的,用3D技術表現只是為了讓故事更生動。對程式而言用內建控制項只是為了更加簡單而不是為了耍酷。另外關於效能方式的選擇請看:http://msdn.microsoft.com/en-us/magazine/dd483292.aspx,即使有時候明知是不好的還是得用,但可以盡量的用更好的。

     關於模式,至少目前為止MVVM從介面脫離程度來講還不如MVP,因為Command的綁定等都需要和WPF相綁定,MVVM模式使得介面也局限於WPF一種。即使你用了類似Caliburn這樣可以不用Command的架構來部分解耦,還是很難和WPF完全分離。

    另外因為時間拖的太久,這個遊戲不得不中止,所以很多地方的做法都表現的很倉促,這個遊戲可以說是只完成了一小部分,甚至可以說只是個開頭的嘗試,或許以後有時間的話再完工了。當然繼續的話估計也是推倒重來了,不管是類設計還是XMAL,還是頁面的美感,它都已經讓我到了忍無可忍的地步。

    PS:不單單這件作品,近來對原來設計的東西是越來越看不順眼,哪都想改,哪都想推倒重做。

    最後感謝吾愛孟夫子對色子的提供。

 

還是給出源碼,希望對有些人用。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.