這是Futurice公司的Windows 8.x 和 Windows Phone 8.x 應用開發者的經驗教訓。通過參考這些指南能夠避免重複造輪子。如果你對iOS或者Android開發感興趣,一定也要去看一下他們的《iOS開發最佳實踐》和《安卓開發最佳實踐》。
歡迎反饋,但是在這之前請先閱讀這個指南。
C# 開發一般都用 Visual Studio
使用Visual Studio Pro或更好的版本
Visual Studio實際上是用來開發Windows應用的IDE。免費的精簡版本對於入門來說不錯,但是缺少一些重要的特性,比如支援額外的擴充套件。專業版本能通過一次安裝提供所有不同專案型別的支援、額外開發特性、支援擴充套件、和一些支援團隊合作的特性。特惠版主要新增了一些優於普通單元測試的內建測試支援,而旗艦版新增了增強的除錯、架構和程式碼分析工具。
使用Productivity Power工具(2013)
來自微軟的一個免費 VS 開發擴充套件。它缺少一些同類產品——比如JustCode或者ReSharper——擁有的特性。不過這似乎不會使IDE的效率變低。
使用NuGet
Nuget是微軟的一個包管理工具。在新的Visual Studio中預裝了一個叫NuGet Package Manager的擴充套件。注意:當不需要在解決方案中包含原始碼時,請把它作為外部引用。
根據NuGet的文件:
自從NuGet2.7開始,當Build開始時,NuGet 的Visual Studio擴充套件整合進了Visual Studio 的Build事件和修復失去的包的過程中。
這種包修復方法有若干優點:
- 不需要專門為你的工程或解決方案去啟用它。Visual Studio 在你的工程在被構建之前就會自動下載缺失的包,並且團隊成員不需要了解NuGet Package Restore的詳細情況。
- 在你的專案或解決方案中不需要額外的目錄或檔案。
- Visual Studio 呼叫 MSBuild 之前前,會恢復(缺失的)包。這可以讓通過目標/屬性檔案匯入的擴充套件 MSBuild 的包在 MSBuild 啟動之前恢復,以確保成功構建(build)。
- 與用Visual Studio建立的ASP.NET 網站專案相容。
如果你點選了Visual Studio中的”Enable NuGet Package Restore”按鈕後你會使用舊的包修復工具。如果是這樣,你應該遷移至:NuGet doc或with pictures.
將程式碼分成小段的方法,以提升堆疊跟蹤和堆疊呼叫檢視的效能
為了可重用性,程式碼通常被分割成小段的方法塊。然而即使不打算重用它們,也應該將方法拆分。方法名記載了程式碼包含的內容。當除錯時這給予你更多關於呼叫堆疊資訊,和更好的異常的堆疊跟蹤資訊。堆疊跟蹤更多應用到釋出版的程式中,在那裡面程式碼行的資訊已經丟失了。
在跟蹤的時候使用caller information屬性
當為任意的引數新增CallerMemberName、CallerFilePath 或者 CallerLineNumber屬性的時候,這個引數的相關資訊將被設定成呼叫者的檔案路徑、行號和成員名。這些值將在編譯時被設定進方法呼叫中,不會影響執行效率,也不會被混淆的程式碼所影響。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// using System.Runtime.CompilerServices // using System.Diagnostics; public void DoProcessing() { TraceMessage("Something happened."); } public void TraceMessage(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) { Trace.WriteLine("message: " + message); Trace.WriteLine("member name: " + memberName); Trace.WriteLine("source file path: " + sourceFilePath); Trace.WriteLine("source line number: " + sourceLineNumber); } // Sample Output: // message: Something happened. // member name: DoProcessing // source file path: c:\Users\username\Documents\Visual Studio 2012\Projects\CallerInfoCS\CallerInfoCS\Form1.cs // source line number: 31 |
為你的HTTP應用使用HTTPClient
在各種各樣的可用的HTTP API中,HttpClient是一個新的支援 非同步/等待 和自動請求解壓的API。通過 非同步/等待的方式來使用它來保持程式碼的簡潔易讀。
瞭解計時器
這裡有為了不同目的的不同的計時器。比如WinRT的DispatcherTimer(定時器),WP Silverlight 的DispatcherTimer 和ThreadPoolTimer。另外還有Observable.Timer,Task.Delay。最後但不是最重要的Thread.Sleep
返回一個IEnumerable的時候,用yield
而不是寫一些像下面這樣的東西:
1 2 3 4 5 6 7 8 9 10 |
public System.Collections.Generic.IEnumerable<Galaxy> Galaxies { get { return new List<Galaxy>() { new Galaxy { Name = "Tadpole", MegaLightYears = 400 }, new Galaxy { Name = "Pinwheel", MegaLightYears = 25 }, new Galaxy { Name = "Milky Way", MegaLightYears = 0 }, new Galaxy { Name = "Andromeda", MegaLightYears = 3 }, }; } } |
而是要這樣寫:
1 2 3 4 5 6 7 8 |
public System.Collections.Generic.IEnumerable<Galaxy> Galaxies { get { yield return new Galaxy { Name = "Tadpole", MegaLightYears = 400 }; yield return new Galaxy { Name = "Pinwheel", MegaLightYears = 25 }; yield return new Galaxy { Name = "Milky Way", MegaLightYears = 0 }; yield return new Galaxy { Name = "Andromeda", MegaLightYears = 3 }; } } |
如果沒有yield返回被呼叫,它會自動返回一些空的IEnumreable。當你想獲取一個IEnumreable的時候這樣做會避免一些空引用異常。當且僅當可能的迭代器需要的時候,這個yield方法才會執行。比如,下面的語句僅僅建立了一個Galaxy例項,避免了一些不必要的執行過程。
1 |
var firstGalaxy = Galaxies.First(); |
將LINQ查詢顯式轉換成集合,以避免不必要的重複運算
可能首先需要了解的關於LINQ的事是,每次訪問查詢結果的時候,它建立的查詢都會被執行。有時候你的確想要這樣。然而,你經常僅僅想要執行這個查詢一次,多次訪問被查詢的結果。為了避免不必要的重複計算以及查詢結果變化帶來的錯誤,請使用ToArray、ToList等擴充套件方法來執行查詢並將結果儲存到集合中。
所以不要這樣做:
1 2 3 4 5 |
var sItems = MyItems.Where(i => i.Name.Contains("s")); var firstSItem = sItems.First(); // Query executed var lastSItem = sItems.Last(); // Query executed MyItems.Add(new MyItem("spoon")); Handle(sItems.Last()); // Query executed, returns the spoon item |
而要這樣做:
1 2 3 4 5 |
var sItems = MyItems.Where(i => i.Name.Contains("s")).ToList(); // Query executed var firstSItem = sItems.First(); var lastSItem = sItems.Last(); MyItems.Add(new MyItem("spoon")); Handle(sItems.Last()); // returns the lastSItem |
不要被IObservable超時蠱惑(這個IObservable資源,IObservable firstTimeout, Func> timeoutDurationSelector)
現在,這是一個介面,所以不同的實現呈現的結果也不一樣。以下的應用都至少是相對於在System.Reactive.Linq.Observable中的實現而言的.
很容易想到應該這樣做:
1 |
.Timeout(Observable.Return(TimeSpan.FromSeconds(10)), vm => Observable.Return(TimeSpan.FromSeconds(1))) |
然而,這樣很容易就超時了.所以正確的使用方法是:
1 2 |
// Notice the .Timer .Timeout(Observable.Timer(TimeSpan.FromSeconds(10)), i => Observable.Timer(TimeSpan.FromSeconds(1))) |
所以,當傳遞的IObservable完成時會發生超時,而不是在傳遞的TimeSpan的持續時間過了以後超時.
Windows App 開發
不要給你的自定義控制元件的名字硬編碼
當編寫自定義或使用者控制元件的時候,不要給控制元件的名字屬性設定一個固定的值.在XAML中這意味著為根節點設定x:Name屬性。
每一個在PresentationFrameworkCollection中的從屬物件必須有一個獨特的名字,並且如果最終在同一個PresentationFrameworkCollection中新增了兩個有同樣名字的控制元件,你會遇到這樣的情況:
1 2 3 4 5 |
{System.ArgumentException: Value does not fall within the expected range. at MS.Internal.XcpImports.CheckHResult(UInt32 hr) at MS.Internal.XcpImports.Collection_AddValue[T](PresentationFrameworkCollection`1 collection, CValue value) at MS.Internal.XcpImports.Collection_AddDependencyObject[T](PresentationFrameworkCollection`1 collection, DependencyObject value) at System.Windows.PresentationFrameworkCollection`1.AddDependencyObject(DependencyObject value) |
當你不硬編碼的時候,框架(framework)會為每一個例項生成一個專屬的名字。
當將屬於一個依賴物件的多個依賴屬性繫結到一起時要格外小心
將會有兩個可能的意外情況:
順序問題
捆綁到DataContext改變了預設的捆綁資源
比如,以下的XAML在MyAdViewModel中而不是在MyPageViewModel尋找AdVisiblity。改變捆綁的順序沒有任何影響。(但是會首先在MyPageViewModel中尋找,然後在MyAdViewModel中尋找)。
1 2 3 4 5 6 7 8 9 10 11 |
<Grid> <Grid.DataContext> <MyPageViewModel AdVisiblity="Collapsed"> <MyPageViewModel.AdContext> <MyAdViewModel Url="www.bystuff.com" Text="Buy Stuff!"/> </MyPageViewModel.AdContext> </MyPageViewModel> </Grid.DataContext> <AdControl Visibility="{Binding AdVisibility}" DataContext="{Binding AdContext}"/> </Grid> |
會發生這種事情是因為”{Binding PropertyName}”是以下的簡寫:
“{Binding Path=DataContext.PropertyName, Source={RelativeSource Self}”
事實上它將 DataContext中的自己的屬性,和在這個物件中的PropertyName屬性繫結在了一起。當DataContext沒有設定時,它會自動從父類繼承。
使用CallerMemberName屬性或者一個LINQ表示式來幫助通知屬性的改變
很多的MVVM框架已經在檢視模型中提醒屬性的改變。不過不要用其中的任何一種,你應該為自己建立一個基礎檢視模型類。但是應當注意建立一個LINQ表示式所造成的效能開銷。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
//using System; //using System.ComponentModel; //using System.Linq.Expressions; //using System.Runtime.CompilerServices; public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Use this to notify a property change from outside of the property's setter. /// For example: NotifyPropertyChanged(() => MyPropertyWhoseGetterShouldNowReturnNewValue); /// </summary> protected void NotifyPropertyChanged<T>(Expression<Func<T>> memberExpression) { var lambda = (memberExpression as LambdaExpression); if (lambda == null) return; var expr = lambda.Body as MemberExpression; if (expr == null) return; NotifyPropertyChanged(expr.Member.Name); } /// <summary> /// Use this from within a property's setter /// For example: if (SetProperty(ref _propertysBackingField, value)){ OptionallyDoThisIfValueWasNotEqual(); } /// </summary> protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName]String propertyName = "") { if (object.Equals(storage, value)) { return false; } storage = value; NotifyPropertyChanged(propertyName); return true; } /// <summary> /// You can use this from within a property's setter when you don't want to set a backing field. /// For example: NotifyPropertyChanged(); /// </summary> protected void NotifyPropertyChanged([CallerMemberName]string propertyName = "") { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } |
如果你在ViewModels中使用Rx,也請使用ReactiveProperties 和 ReactiveCommands
如果你還沒有用過能方便地從XAML中繫結互動程式碼的庫的話,去搜尋一下ReactiveProperty 和 ReactiveCommand 幫助類。或者直接在這裡面選用一個由Tomasz Polanski建立的 reactive工具庫。