今天把JDi/Server測試作完.終於有了時間來寫寫關於這個項目總結.關於我在博客上Post這些文章內容都是從實際項目應用而來.固然有些問題解決方案也是不斷被重複設計修改.期間也碰到諸多問題.也曾爲客戶端在UI設計和具體的實現倍感困惑過.下午在Product Ower UI原型設計討論會上. 設計團隊針對內部一個孵化SNS項目原型設計作了三套設計方案.從IPhone到Android 再到Windows phone.頓時不由有一個疑問. 若是拋開市場定位 用戶羣體.和業務需求等等.單單從開發人員角度來講 什麼樣的APP才能稱之爲一個用戶可以接受並樂於使用的呢?前端
在發表討論前 注意我這個命題成立條件[如上加粗字體].若是隻看後半段.固然不少市場.運營 甚至是最終用戶都會否掉這個命題的成立條件.本篇暫且假設它是成立的.所以我在內部的Wiki上向IOS和Android WP團隊發起一個討論.把這個討論結果生成一個Wish List 按照優先級作了排列 以下:sql
App Wish List:數據庫 [1]:貼近用戶自身實用的功能 編程 [2]: APP自身的穩定性json [3]: 良好的用戶交互體驗緩存 [4]: 創意服務器 ……網絡 |
從一個數據列表來以小見微來講明這個問題.:異步
在Windows phone中目前獲取動態數據列表數據方式.雲端/IsolataSTorage/網絡請求/Socket通訊/SQLCED等.做爲常見客戶端而言.最多采用就是網絡請求的方式來從服務器端拉去數據.在採用 WebClient或HttpWebREquest方式或是雲端存儲 涉及網絡請求數據時.在Begin-End異步處理模型中應首先保證咱們代碼塊作了正確的事.並可以在出現異常時也能正常執行流程反饋在UI上. 一個APP健壯性在反饋編碼上最小層級就是代碼塊異常處理. ide
可是在異常處理中不少Dev都關注程序自身功能性Bug.客戶端明肯定義是可以處理自身以及和外界網絡發生交互時任何Exception.請求網絡中斷. 請求超時. 服務器端Server返回404 Not Found. 數據解析異常等等.這都須要客戶端處理並UI中有所呈現.代碼塊的完整異常處理是APP健壯性最小單位.
對於比較常見或是不可見的異常能夠經過服務器端統一過濾編碼並明確返回客戶端提示.這樣作目的是爲了保證不把程序異常直接堆棧信息暴露給用戶. 並採用客戶端日誌記錄. 固然在客戶端異常存在某種條件下照成不可見的APP崩潰的狀況. 因此在UnHandlerException()方法中當APP崩潰必須退出時也得給用戶一個友好的退出提示.
now.當順利拿到數據.經過最經常使用的ListBox來呈現.在DataTemplate數據模板中包含文字和圖片信息.在呈現文字信息以前加載時間內必須給出動態加載進度條. 但注意在用戶操做BackUpPress鍵時必要處理.
在數據呈現時.這裏必須提到ListBox自身性能問題.當經過綁定ItemSource數據源時,若是呈現ObserverCollection<T>沒有限定數量.會照成ListBox在UI操做發生延遲或是得不到及時響應.ObserverCollection<T>若是存在10000數據項.在後臺中Silverlight必須將其實例化10000個ListItem實例項.才能經過UI呈現出現.短期APP內存使用劇增.出現性能瓶頸.
因此綁定時數據源ObserverCollection<T>應儘量的小的數據量20-30.儘可能減小每次更新代價. 若是沒法避免相似在須要大型數據綁定時 能夠考慮在後臺實現動態加載數據源方式.在數據呈現過程當中請慎用ValueConverter轉換器.
ValueConverter轉換器慎用: ValueConverter轉換器採用自定義代碼實現轉換數據格式或額外操做. 致使沒法在實際元素呈現以前肯定頁面佈局和數據緩存. 特別是在出現大數量時ValueConverter簡直就是延時器. 可能致使UI線程長時間阻塞得不到響應.大大下降用戶體驗.請慎用! |
在動態加載實現上能夠參考IPhone加載數據列表的方式.當用拖拽數據列表到達底部時則動態Loading數據.儘可能保證UI可操做.不要讓用戶把時間浪費Loading等待上.
說到這想起奇藝客戶端不由想吐槽一下[善意的:) 奇藝客戶端剛出第一個版本時 經過首頁進入到在Pivot樞軸呈現的同步劇場界面時[沒有截到圖]:
在Pivot控件中用戶左右移動PivotItem頁時老是提示用戶Loading界面. 要知道用戶沒有多少耐心去等待.並且用戶一發生PivotItem切換是都會加載數據過程.當你在徹底斷開網絡鏈接狀況.在打開客戶端時你會發現裏面沒有任何數據……這證實每次用戶操做數據都是即時的. 用戶體驗不太友好 用戶指望:
A:第一次能夠容許進行加載數據.但當再次打開界面應具備可操做數據. B:每次切換PivotItem樞軸頁加載等待時間這個Loading…太不合時宜了.若是已經加載過應存在客戶端中.當存在數據更新時才動態更新列表數據. |
數據緩存對於數據列表價值主要體如今友好的用戶交互體驗上.因此一個健全的客戶端是沒法缺乏一個有效緩存系統支撐的.數據緩存在客戶端中主要解決兩個問題:
緩存解決的問題: [1]:效率,緩存自己就是爲了提升性能,不要由於緩存的緣由反而減低了性能 [2]:數據的實時性,對於緩存數據的實時性,各類緩存設計都有本身的策略,好比設置過時時間、定時刷新等。具體採用哪一種策略和具體的業務不無關係 |
數據列表緩存: A:客戶端可以存數據保證每次在斷開鏈接狀況有數據能夠操做.實現數據緩存 B:可以實現數據列表以動態更新.提升UI用戶交互體驗 |
針對緩存需求設計緩存以下:
如上緩存設計則採用以單一過時時間做爲緩存策略.並把該緩存更新時間的選項暴露給用戶選擇. 用戶也能夠清空本地客戶端緩存數據:
ok.從如上設計圖不難看出.當第一次發起請求時作了一方面把數據還回客戶端同時在客戶端創建數據緩存.當頁面再次加載時檢查緩存時間是否過時.若是已通過期則從新請求服務器拉取數據.並更新本地數據緩存數據.其實在設計這個緩存時.客戶端緩存更新須要暴露兩個接口.一個利用緩存時間策略有客戶端自動更新.固然做爲用戶也須要把更新列表和緩存的選項以刷新的方式暴露出來 例如在列表ApplicationBar下添加刷新操做:
可是整體來看這種更新緩存的方式咱們來總結這種緩存設計的優缺點:
緩存設計優缺點: A:緩存更新策略單一.更新緩存方式統一 編程容易控制.能知足基本的客戶端數據緩存操做. B:在緩存時間內存在沒法獲取即時數據缺陷. |
這個設計存在一個缺陷就是在緩存存儲時間內.服務器端更新的數據在用戶沒有操做刷新按鈕的狀況下.沒法即時獲取服務器端最新數據.客戶端只能經過不斷輪詢的方式來實現數據更新.這種方式主要因服務器沒有創建主動數據更新消息推送機制致使.更新間隔的頻率能夠保持在30S內一次操做. 在Windows phone 中針對這種狀況採用Push Notification推送通知的機制來完善這個缺陷:
在使用Windows phone Push Notification推送通知服務.服務器端創建一個WebService[WCF服務]當服務器端數據發生更新時則經過WCF 服務向雲端的推送通知服務發送一條數據更新的消息.由推送通知服務把消息響應給客戶端.經過消息處理程序客戶端解析數據更新消息後.從新鏈接服務器主動加載數據.並更新本地客戶端緩存.
推送通知具體工做流程以下:數字具體的工做步驟
可是這種方式在實際應用中也存在具體的幾個問題.在客戶端目的就是就是服務器端可以在數據更新時創建一種主動向客戶端推送通知機制. 通知客戶端數據已經更新.由客戶端從新發起數據請求更新本地緩存.Push Notification推送服務徹底可以完成這項工做.可是在實際測試中依然發現一個問題. 其實在設計時咱們忽略Windows phone 推送通知處理方式是批處理方式傳遞.
處理的事務可能不是即時的。 推送通知的及時性將得不到保證,並且將由該推送通知服務決定什麼時候將通知傳遞給客戶端;只能被動的經過數據更新方式經過監聽服務來觸發通知. |
可是這種好處在創建主動更新的機制.
測試中發現.在服務器端把全部數據都作壓縮處理.也就是每次請求數據對服務器代價很低.若是沒有涉及大數量和即時更新要求.第一種輪詢緩存設計足以夠用.推送通知這種方式主要在創建一種主動更新的機制.把更新的操做交給應用程序後臺來作.但在必要的時候則徹底越過推送通知方式 直接訪問WCF服務來檢測數據是否更新.也是可行的.
如上討論客戶端緩存實現設計兩種方式.各有利弊.針對這種方式 先實現輪詢的方式數據緩存的設計.實際項目中.當創建客戶端緩存時把數據列表的數據序列化後一Json文件的方式存在獨立存儲空間內.並對json文件進行管理.IsolateStroage開闢一塊獨立的區域.當咱們一個APP設計多個模塊使用數據緩存.在同一個層級上管理多個Json文件就會發生混亂.
針對這種狀況.能夠採用以模塊爲分類在獨立存儲空間創建文件夾目錄.對應目錄下放着該模塊緩存全部數據Json文件.這樣對獨立存儲強制的分區的目的主要有兩個.一個便於文件管理和分類. 另一個就是減小數據訪問查找範圍提升讀寫大文件時性能.
在BuyTicket模塊.當拿到數據文件並完成序列化成Json格式文件.執行存儲時格式是:
Cache Json File: 「[模塊目錄名稱]/[序列化Json文件名稱].Txt」 |
發如今編程中直接操做Json文件或目錄極容易出錯.咱們把每個創建緩存文件封裝成實體.而後真正文件操做前經過IsolateStorageSeting字典表於具體的CacheEntity緩存實體進行關聯.那麼編程操做就是是封裝的緩存實體CacheEntity對象.在以緩存時間單一策略下CacheEntity須要以下屬性:
- [DataContract]
- public class CacheEntity
- {
- /// <summary>
- /// 緩存起始時間
- /// </summary>
- [DataMember]
- public string StartDate { get; set; }
- /// <summary>
- /// 緩存週期
- /// </summary>
- [DataMember]
- public string CacheDate { get; set; }
- /// <summary>
- /// 惟一存儲Key
- /// </summary>
- [DataMember]
- public string CacheKey { get; set; }
- /// <summary>
- /// 存儲模塊
- /// </summary>
- [DataMember]
- public string CacheDirName { get; set; }
- /// <summary>
- /// 存儲文件名稱
- /// </summary>
- [DataMember]
- public string CacheFileName { get; set; }//文件名稱
- /// <summary>
- /// 緩存數據類型
- /// </summary>
- [DataMember]
- public string CacheContext { get; set; }//緩存數據類型
- }
CacheEntity緩存實體類中StartData標識緩存開始的時間. DataCache則是用戶決定緩存週期.設置有默認值. 惟一存儲Key則用來標識在IsolateStorageSeting中標識CacheEntity.而緩存數據類型CaCheContext是在獲取Json文件後反序列化時須要指定源數據反序列類型.
有了CacheEntity封裝則能夠在創建一個CacheManager容器來管理緩存數據. CacheManager中要實現緩存實體的CRUD 以外要設置緩存週期等 添加一個數據緩存:
- public class CacheManager
- {
- /// <summary>
- /// 緩存是否過時
- /// </summary>
- /// <param name="getCacheEntity">Cache Entity</param>
- /// <returns>Is out Of Date</returns>
- public static bool CacheEntityIsOutDate(CacheEntity getCacheEntity)
- {
- bool isOutOfDate = false;
- if (getCacheEntity != null)
- {
- DateTime currentDate = DateTime.Now;
- TimeSpan getTimeSpan = currentDate - Convert.ToDateTime(getCacheEntity.StartDate);
- int compareValue = getTimeSpan.CompareTo(new TimeSpan(0, Convert.ToInt32(getCacheEntity.CacheDate), 0));
- if (compareValue == -1)
- isOutOfDate = false;//未過時
- else
- isOutOfDate = true;//過時
- }
- return isOutOfDate;
- }
- /// <summary>
- /// 添加緩存
- /// </summary>
- /// <param name="getCacheEntity">cache Entity</param>
- public static bool AddCacheEntity(CacheEntity getCacheEntity)
- {
- bool isCache = false; 34: if (getCacheEntity != null)
- isCache=UniversalCommon_operator.AddIsolateStorageObj(getCacheEntity.CacheKey,getCacheEntity);
- return isCache;
- }
- }
能夠看到經過CacheManager可以實現對緩存CacheEntity字典表管理方式.這樣大大簡化咱們編程直接操做數據文件複雜性.有了CacheManager後還須要對底層Json文件進行管理定義一個FileManager類.在BuyTicket模塊添加一個TicketList數據緩存.這是須要在IsolateStorage獨立存儲建立BuyTicket文件夾並在該文件夾下創建一個TicketListJson.txt格式Json文件.添加文件操做:
- public class FileManager
- {
- /// <summary>
- /// 建立文件目錄
- /// </summary>
- /// <param name="dirName">目錄名稱</param>
- public static bool CreateDirectory(string dirName)
- {
- bool isCreateDir = false;
- if (!string.IsNullOrEmpty(dirName))
- {
- using (IsolatedStorageFile getIsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())
- {
- if (!getIsolatedStorageFile.DirectoryExists(dirName))
- {
- //Not Exist And CreatDir
- getIsolatedStorageFile.CreateDirectory(dirName);
- isCreateDir = true;
- }
- }
- }
- return isCreateDir;
- }
- /// <summary>
- /// 建立文件
- /// </summary>
- /// <param name="dirname">目錄名稱</param>
- /// <param name="filename">文件名稱</param>
- /// <param name="getDataStream">文件內容</param>
- /// <returns>是否建立</returns>
- public static bool CreateFile(string dirname, string filename, Stream getDataStream)
- {
- bool isCreateFile = false;
- if (!string.IsNullOrEmpty(filename))
- {
- #region No Directory
- using (IsolatedStorageFile getIsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())
- {
- string filepath = filename + ".txt";
- if (getIsolatedStorageFile.FileExists(filepath))
- getIsolatedStorageFile.DeleteFile(filepath);//Exist To Delete
- //Create File
- isCreateFile = true;
- getIsolatedStorageFile.CreateFile(filepath);
- getDataStream.Seek(0, SeekOrigin.Begin);
- //Write Data
- using (StreamReader getReader = new StreamReader(getDataStream))
- {
- using (StreamWriter getWriter = new StreamWriter(getIsolatedStorageFile.OpenFile(filepath, FileMode.Open, FileAccess.Write)))
- {
- getWriter.Write(getReader.ReadToEnd());//Save Data
- }
- }
- }
- #endregion
- }
- return isCreateFile;
- }
- }
這樣咱們就是先對緩存和底層操做IsolateStorage獨立存儲空間Json文件進行管理. Now.在ListCache要加載這些數據並添加緩存中.在View_model操做以下:
- public void LoadDataCacheFilmTicketDate()
- {
- this.dataCacheTicketCol.Clear();
- //假設訪問網絡鏈接 得到以下鏈接數據
- this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "猩球崛起", TicketPrice = "80" });
- this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "青蜂俠", TicketPrice = "180" });
- this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "拯救大兵瑞恩", TicketPrice = "80" });
- this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "Secret Men", TicketPrice = "80" });
- //添加緩存
- #region 緩存存儲
- if (dataCacheTicketCol.Count > 0)
- {
- List<FilmTicket> getTickelist = new List<FilmTicket>();
- foreach (FilmTicket getFilmTicket in dataCacheTicketCol)
- getTickelist.Add(getFilmTicket);
- if (getTickelist.Count > 0)
- {
- using (MemoryStream getStreamObj = new MemoryStream())
- {
- //Cache Store Entity
- CacheEntity ReBackEntity = new CacheEntity()
- {
- StartDate = DateTime.Now.ToString(),
- CacheKey = "FilmIndexList",
- CacheDate = CacheManager.SettingCacheDate(),
- CacheContext = typeof(List<FilmTicket>).ToString(),
- CacheDirName = "BuyTicket",
- CacheFileName = "FilmIndexList"
- };
- CacheManager.AddCacheEntity(ReBackEntity);
- //Store Cache File
- Serialiser.PartSerialise(getStreamObj, getTickelist);
- bool isCache = FileManager.CreateFile("BuyTicket", "FilmIndexList", getStreamObj);
- //Store Cache Status
- UniversalCommon_operator.AddIsolateStorageObj("CacheFilmIndexList", true);
- }
- }
- }
- #endregion
- }
在添加數據緩存時.首先創建一個CacheEntity實體數據.分別設置緩存週期,開始時間默認爲如今. 存儲數據以及存儲數據類型List<FilmTicket>. 以便在反序列化經過反射方式來生成數據集合.並把該實體添加CacheManager管理容器中. 添加緩存後須要對底層Json執行存儲操做.並與CacheEntity進行關聯.關聯的方式是Key命名和CacheFileName一致.最後表示緩存存儲的狀態.
緩存創建成功.在下一次加載列表時會檢查獨立存儲空間緩存數據.若是沒有過時則加載獨立存儲中緩存數據. 加載緩存數據:
- #region 加載緩存
- if (UniversalCommon_operator.IsolateStorageKeyIsExist("FilmIndexList"))
- {
- CacheEntity getcacheEntity = CacheManager.QueryCacheEntityObj("FilmIndexList");
- if (getcacheEntity != null)
- {
- #region Cache Date Operator
- if (CacheManager.CacheEntityIsOutDate(getcacheEntity))
- getTicketIndex_ViewModel.LoadCurrentFilmList();//過時
- else
- {
- //Read date from cache
- string jsonStr = FileManager.ReadFile(getcacheEntity.CacheDirName, getcacheEntity.CacheFileName);
- if (!string.IsNullOrEmpty(jsonStr))
- {
- //Assembly get Object Type
- MemoryStream getJsonStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(jsonStr));
- Assembly currentAssembly = Assembly.Load("WelfareLife.Common");
- Type currentType=currentAssembly.CreateInstance(getcacheEntity.CacheContext).GetType();
- List<FilmTicket> getTicketlist = Serialiser.PartDeSerialise(currentType, getJsonStream) as List<FilmTicket>;
- if (getTicketlist.Count > 0)
- {
- //清空數據
- this.getTicketIndex_ViewModel.filmTicketCollection.Clear();
- getTicketlist.ForEach(x => getTicketIndex_ViewModel.filmTicketCollection.Add(x));
- }
- }
- }
- #endregion
- }
- }
- #endregion
首先經過CacheEntity獲取緩存Json格式數據.經過反射的方式獲取反序列化轉換數據類型.轉換成功後 更新ViewModle中ObserverCollection<T>集合數據.並反饋到UI上.這樣就成功獲取緩存中存儲數據.若是緩存已通過期.則從新請求服務器端數據.並更新本地緩存.這樣一個簡單以緩存週期爲單一策略的緩存構建完成.因爲本片篇幅有限.關於推送通知的設計的緩存將再也不本篇說明.若有要源碼請Email我.
如上從Windows phone APP穩定性和用戶交互體驗以一個數據列表方式從代碼塊異常處理,.數據性能.以及客戶端緩存系統構建3個角度.來講明這個問題.篇幅畢竟有限.本篇核心放在緩存系統構建方案設計和實現上.實際操做場景中.第一種輪詢的方式簡單實用.編程控制統一.而關於推送通知這種方式在服務器端交付時並沒有差別.可是相對比較複雜.對於比較頻繁或對服務器要求比較APP中則纔會體現與輪詢上優點. 一個具備良好用戶體驗的客戶端對一個完善緩存系統是依賴的.固然咱們其實作了四種方案.本文中只是拿了最爲特殊兩個方案進行比對.歡迎各位提供更好的解決方案.
本篇源碼見附件。
Windows phone應用開發: |
相對於PC端數據緩存設計思路可選擇性在移動客戶端並非特別多. 主要由於移動客戶端可以使用資源和用戶需求都遠低於Pc端Application.相似WP中可以用來支持數據存儲SQLCE數據庫和IsolateStorage獨立存儲空間.等徹底沒有Pc端應用程序開放性.從一個數據列表來講對緩存要求極爲簡單:
so.良好的用戶體驗 是創建在客戶端數據緩存基礎之上的……
可能這個排列對最終用戶而言存在爭議. 在Wiki中不少人提到創意應該走在APP最前端.也就是設計階段時應該考慮到.可是若是從開發角度而言.不管什麼樣的創意一旦從開發流程來執行時.並沒有任何差別.開發流程在每個固定的MileStore里程碑中可以持續交付功能.並能保證整個APP自身穩定性.再次基礎上創建良好的UI用戶交互體驗.整個APP開發使命也就算基本完成了.
若是對於Windows phone的APP知足上面的WishList.須要注意哪些方面問題?
針對這個問題.開發者很容易陷入各類各樣開發細節之中.這些細節大可能是他們眼前要面對或即將要解決的問題.而沒法從這些細節的泥沼之中抽身出來從整個APP全局角度來思考這個問題.那針對如上APP須要 和本身理解.側重APP穩定性和用戶交互體驗.能夠經過如下幾點來切入.
這些問題從Windows phone 一個數據列表提及.