終於要邁進Universal的大坑了,還有點小激動呢。
CurrencyExchanger 掌中匯率是我前幾年發布在Windows Phone商店中的一個應用,當時是WP7版本,下載鏈接:http://www.windowsphone.com/zh-cn/store/app/%E6%8E%8C%E4%B8%AD%E6%B1%87%E7%8E%87free/84e93a20-cefb-460f-b0d9-a57689b33c10
已經很久沒有升級了,最近想學習一下Universal開發,就拿這個練練手吧。之前一直沒有系統的寫過文章,現在從頭把開發中的一些過程記錄一下,也是對自己的一個促進。因為是邊做邊寫,肯定會有錯誤,請大家不吝賜教。
一、新建項目
我使用了MVVM-Sidekick框架,這是一個簡單但功能強大的MVVM框架,由微軟的@韋恩卑鄙 開發,我一直用這個框架開發WP8的程序,前不久作者升級支持了Universal App。
新建項目前需要先安裝MVVM-Sidekick的VS擴展插件,在VS2013update2的工具-擴展與更新菜單中搜索mvvm-sidekick就可以找到這個擴展,下載安裝即可。安裝后會添加項目模板和代碼段,比較方便。
github:https://github.com/waynebaby/mvvM-Sidekick
vs插件:http://visualstudiogallery.msdn.microsoft.com/ef9b45cb-8f54-481a-b248-d5ad359ec407
現在可以新建項目了,選擇通用應用程序,MVVM-Sidekick Universal App項目模板,輸入CurrencyExchanger,等待VS建立項目。這個地方有個需要注意,項目名稱不能太長,我第一次輸入了一個比較長的名字,結果VS提示名稱太長,建立失敗了。
二、項目結構
現在可以看到VS2013為我們生成了三個項目,
CurrencyExchanger.Windows
CurrencyExchanger.WindowsPhone
CurrencyExchanger.Shared
可以看到我們熟悉的App.xaml文件被放到了Shared項目中,打開看一下,
#if WINDOWS_PHONE_APP private TransitionCollection transitions; #endif
原來有好多條件編譯啊,通過這種方式來區分Win8.1和WP8.1,有點坑啊。
在OnLaunched方法中,有這么一行:
//Init MVVM-Sidekick Navigations: Startups.StartupFunctions.RunAllConfig();
然后我們找到對應的文件看一下,
public static void RunAllConfig() { typeof(StartupFunctions) .GetRuntimeMethods() .Where(m => m.Name.StartsWith("Config") && m.IsStatic) .Select( m => m.Invoke(null, Enumerable.Empty<object>().ToArray())) .ToArray(); }
這個方法對View和ViewModel進行了配置,以后新加View的話,MVVM-Sidekick會自動添加所需的ViewModel,並在這個類中進行注冊,方便使用。
ViewModel文件夾中放着所需的VM,這個文件夾也是在Shared項目中,說明我們可以只用共享的VM去作為不同平台的View的DataContext,實現了共享代碼的目的。
然后看MainPage_Model.cs這個vm,這個類繼承了ViewModelBase<MainPage_Model>,ViewModelBase是MVVM-Sidekick的重要的一個類,所有的vm都要繼承這個類。里面有一個屬性Title,可以看到還帶着一大坨代碼,這一大坨代碼是怎么出來的呢,MVVM-Sidekick提供了代碼段來幫助生成,所以這就是安裝VS擴展的好處。
通過輸入 propvm ,按Tab,就會自動生成一個屬性,可以方便的綁定到View上了。
然后我們看CurrencyExchanger.WindowsPhone項目中的MainPage.xaml,里面有這么一行:
<Page.Resources> <!-- TODO: Delete this line if the key AppName is declared in App.xaml --> <vm:MainPage_Model x:Key="DesignVM"/> </Page.Resources>
定義了一個資源,把VM引入進來。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" DataContext="{StaticResource DesignVM}"> <TextBlock TextWrapping="Wrap" x:Name="pageTitle" Grid.Column="1" Text="{Binding Title}" Style="{StaticResource HeaderTextBlockStyle}"/> </Grid>
把這個VM作為Grid的DataContext,這樣就可以進行綁定了,可以看到有一個TextBlock的Text屬性綁定到了VM的Title字段。
大體的項目結構就是這樣,下面我們就開始升級。說是升級,其實就是重新開發啊5555
三、建立所需的Model
貨幣轉換這個app功能就是從雅虎財經獲取不同的貨幣代碼直接的匯率,因此首先來建立相應的Model。
在CurrencyExchanger.Shared項目中新建一個Models文件夾,添加一個CurrencyItem.cs,內容如下:

public class CurrencyItem : BindableBase<CurrencyItem> { /// <summary> /// 貨幣代碼 /// </summary> public string Code { get { return _CodeLocator(this).Value; } set { _CodeLocator(this).SetValueAndTryNotify(value); } } #region Property string Code Setup protected Property<string> _Code = new Property<string> { LocatorFunc = _CodeLocator }; static Func<BindableBase, ValueContainer<string>> _CodeLocator = RegisterContainerLocator<string>("Code", model => model.Initialize("Code", ref model._Code, ref _CodeLocator, _CodeDefaultValueFactory)); static Func<string> _CodeDefaultValueFactory = () => { return default(string); }; #endregion /// <summary> /// 描述 /// </summary> public string Description { get { return _DescriptionLocator(this).Value; } set { _DescriptionLocator(this).SetValueAndTryNotify(value); } } #region Property string Description Setup protected Property<string> _Description = new Property<string> { LocatorFunc = _DescriptionLocator }; static Func<BindableBase, ValueContainer<string>> _DescriptionLocator = RegisterContainerLocator<string>("Description", model => model.Initialize("Description", ref model._Description, ref _DescriptionLocator, _DescriptionDefaultValueFactory)); static Func<string> _DescriptionDefaultValueFactory = () => { return default(string); }; #endregion /// <summary> /// 圖片名稱 /// </summary> public string Image { get { return _ImageLocator(this).Value; } set { _ImageLocator(this).SetValueAndTryNotify(value); } } #region Property string Image Setup protected Property<string> _Image = new Property<string> { LocatorFunc = _ImageLocator }; static Func<BindableBase, ValueContainer<string>> _ImageLocator = RegisterContainerLocator<string>("Image", model => model.Initialize("Image", ref model._Image, ref _ImageLocator, _ImageDefaultValueFactory)); static Func<string> _ImageDefaultValueFactory = () => { return default(string); }; #endregion }
這個Model要繼承BindableBase<CurrencyItem>,在MVVM-Sidekick中BindableBase是和ViewModelBase一樣重要的幾個基類,用於實現可綁定的model,但區別是ViewModelBase中還會放一些Command,而BindableBase顧名思義僅用於綁定屬性,不建議在里面放Command這些東西。不要看上面這么一大坨,其實就輸入了幾個單詞而已,都是用propvm生成的。主要是三個屬性,貨幣代碼,描述,圖片名稱。圖片用於在顯示貨幣的時候顯示一個國旗的圖片。
與此類似再建立一個貨幣轉換的model,新建CurrencyExchangeItem.cs文件,代碼如下:

public class CurrencyExchangeItem : BindableBase<CurrencyExchangeItem> { /// <summary> /// 日期 /// </summary> public DateTime TradeDate { get { return _TradeDateLocator(this).Value; } set { _TradeDateLocator(this).SetValueAndTryNotify(value); } } #region Property DateTime TradeDate Setup protected Property<DateTime> _TradeDate = new Property<DateTime> { LocatorFunc = _TradeDateLocator }; static Func<BindableBase, ValueContainer<DateTime>> _TradeDateLocator = RegisterContainerLocator<DateTime>("TradeDate", model => model.Initialize("TradeDate", ref model._TradeDate, ref _TradeDateLocator, _TradeDateDefaultValueFactory)); static Func<DateTime> _TradeDateDefaultValueFactory = () => { return default(DateTime); }; #endregion /// <summary> /// 匯率 /// </summary> public double Rate { get { return _RateLocator(this).Value; } set { _RateLocator(this).SetValueAndTryNotify(value); } } #region Property double Rate Setup protected Property<double> _Rate = new Property<double> { LocatorFunc = _RateLocator }; static Func<BindableBase, ValueContainer<double>> _RateLocator = RegisterContainerLocator<double>("Rate", model => model.Initialize("Rate", ref model._Rate, ref _RateLocator, _RateDefaultValueFactory)); static Func<double> _RateDefaultValueFactory = () => { return default(double); }; #endregion /// <summary> /// 逆向匯率 /// </summary> public double InverseRate { get { return _InverseRateLocator(this).Value; } set { _InverseRateLocator(this).SetValueAndTryNotify(value); } } #region Property double InverseRate Setup protected Property<double> _InverseRate = new Property<double> { LocatorFunc = _InverseRateLocator }; static Func<BindableBase, ValueContainer<double>> _InverseRateLocator = RegisterContainerLocator<double>("InverseRate", model => model.Initialize("InverseRate", ref model._InverseRate, ref _InverseRateLocator, _InverseRateDefaultValueFactory)); static Func<double> _InverseRateDefaultValueFactory = () => { return default(double); }; #endregion /// <summary> /// 是否為基准貨幣 /// </summary> public bool IsStandard { get { return _IsStandardLocator(this).Value; } set { _IsStandardLocator(this).SetValueAndTryNotify(value); } } #region Property bool IsStandard Setup protected Property<bool> _IsStandard = new Property<bool> { LocatorFunc = _IsStandardLocator }; static Func<BindableBase, ValueContainer<bool>> _IsStandardLocator = RegisterContainerLocator<bool>("IsStandard", model => model.Initialize("IsStandard", ref model._IsStandard, ref _IsStandardLocator, _IsStandardDefaultValueFactory)); static Func<bool> _IsStandardDefaultValueFactory = () => { return default(bool); }; #endregion /// <summary> /// 貨幣數量 /// </summary> public double Amount { get { return _AmountLocator(this).Value; } set { _AmountLocator(this).SetValueAndTryNotify(value); } } #region Property double Amount Setup protected Property<double> _Amount = new Property<double> { LocatorFunc = _AmountLocator }; static Func<BindableBase, ValueContainer<double>> _AmountLocator = RegisterContainerLocator<double>("Amount", model => model.Initialize("Amount", ref model._Amount, ref _AmountLocator, _AmountDefaultValueFactory)); static Func<double> _AmountDefaultValueFactory = () => { return default(double); }; #endregion /// <summary> /// 基准貨幣 /// </summary> public CurrencyItem CurrencyBase { get { return _CurrencyBaseLocator(this).Value; } set { _CurrencyBaseLocator(this).SetValueAndTryNotify(value); } } #region Property CurrencyItem CurrencyBase Setup protected Property<CurrencyItem> _CurrencyBase = new Property<CurrencyItem> { LocatorFunc = _CurrencyBaseLocator }; static Func<BindableBase, ValueContainer<CurrencyItem>> _CurrencyBaseLocator = RegisterContainerLocator<CurrencyItem>("CurrencyBase", model => model.Initialize("CurrencyBase", ref model._CurrencyBase, ref _CurrencyBaseLocator, _CurrencyBaseDefaultValueFactory)); static Func<CurrencyItem> _CurrencyBaseDefaultValueFactory = () => { return default(CurrencyItem); }; #endregion /// <summary> /// 目標貨幣 /// </summary> public CurrencyItem CurrencyTarget { get { return _CurrencyTargetLocator(this).Value; } set { _CurrencyTargetLocator(this).SetValueAndTryNotify(value); } } #region Property CurrencyItem CurrencyTarget Setup protected Property<CurrencyItem> _CurrencyTarget = new Property<CurrencyItem> { LocatorFunc = _CurrencyTargetLocator }; static Func<BindableBase, ValueContainer<CurrencyItem>> _CurrencyTargetLocator = RegisterContainerLocator<CurrencyItem>("CurrencyTarget", model => model.Initialize("CurrencyTarget", ref model._CurrencyTarget, ref _CurrencyTargetLocator, _CurrencyTargetDefaultValueFactory)); static Func<CurrencyItem> _CurrencyTargetDefaultValueFactory = () => { return default(CurrencyItem); }; #endregion }
這個model就是用來顯示貨幣匯率轉換的,里面有兩個貨幣的信息還有匯率的信息等等。
四、初始化數據
在用戶第一次進入app時,應該讓用戶選擇要顯示哪些貨幣的匯率,這樣就要給用戶提供一個貨幣列表,這個列表需要我們提前初始化好。
新建一個Context類,放一些常用的東東。在Shared項目中新建Utilities目錄,添加一個Context.cs文件,做成單例。

public sealed class Context { static readonly Context instance = new Context(); static Context() { } private Context() { } /// <summary> /// Gets the instance. /// </summary> /// <value>The instance.</value> public static Context Instance { get { return instance; } } }
在里面添加一個列表:
public List<CurrencyItem> AllCurrencyItemList { get; set; }
然后一個初始化方法:
public void Init() { AllCurrencyItemList = new List<CurrencyItem>() { new CurrencyItem{Code = "AED", Description ="阿聯酋迪拉姆", Image="flag_united_arab_emirates"}, new CurrencyItem{Code = "ALL", Description = "阿爾巴尼亞列克", Image="flag_albania"}, …… }
找到App.xaml.cs,在OnLaunched方法中調用此方法:
//Init Context Context.Instance.Init();
添加貨幣列表是一個很枯燥的工作,當初我是從雅虎財經網頁上扒下的貨幣代碼,又從網頁素材網站找到國旗的圖片,挨個整理好。當然也可以事先整理成xml來讀取。
慢着,我的WP7程序就是支持多語言的,此時當然不能把貨幣描述直接hard code,而應該從資源文件中按照用戶當前的語言來顯示。
好吧又多了一個問題,多語言。
五、可以叫全球化多語言本地化……反正就是可以讓用戶選擇語言
以前的WP7多語言需要自己搞一大坨代碼,到了WP8方便了一點,VS會幫助干很多事。但到了Universal,情況又變了。WP8添加資源文件的時候資源文件格式為resx,同時程序會自動添加一個AppResouces.Designer.cs,通過一個全局的ResourceManager去取得資源文件中的字符串,代碼中可以直接調用:
String appName = AppResources.AppName;
是不是很方便?
到了Universal里,自動生成的沒有了,添加的資源文件格式變成了resw,需要用這種方式來調用:
var loader = new Windows.ApplicationModel.Resources.ResourceLoader(); var string = loader.GetString('Farewell');
是不是很坑?萬一字符串寫錯了就找不到了。
添加多語言文件倒不麻煩,有多語言工具包,鏈接:http://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/jj572370.aspx
但是調用顯得不太友好。所以我仿照WP8的方式新建了一個AppResources.cs,放到Utilities,里面這樣寫:
public static class AppResources { public static ResourceLoader CurrentResourceLoader { get { return ResourceLoader.GetForCurrentView(); } } public static string AppName { get { return CurrentResourceLoader.GetString("AppName"); } } 。。。。。。 }
只要保證這里寫對,這樣以后調用的時候就不怕出錯了。
多語言資源文件的添加比較簡單,有工具包協助,甚至翻譯都可以幫你做好,具體步驟見
http://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/jj572370.aspx
需要注意的是,以前的方式需要我們為每種語言建立一個資源文件,現在有多語言工具包就不需要了,只添加一個默認語言的即可,工具包會自動填充其他的語言。比如CurrencyExchanger默認語言是英語,那么步驟就是:
打開Package.appxmanifest文件,把默認語言改成en-US,然后添加一個Strings文件夾,下面添加en-US文件夾,添加一個Resources.resw資源文件,在這里面編輯所需要的字符串。
右鍵單擊CurrencyExchanger.WindowsPhone,選擇添加翻譯語言,
這樣會自動建立一個MultilingualResources文件夾,里面是一大坨xlf后綴的文件,qps-ploc.xlf這個是偽語言,用於測試的,在其他的幾個文件上點右鍵,選擇打開方式,選擇多語言編輯器,出來這么一個東東:
看到菜單沒有,點翻譯,Microsoft Translator直接就幫你翻譯好了。當然還需要進一步校對,但已經很智能化了。這樣就不需要為每種語言建資源文件了,可以從這些xlf文件里找。需要注意的是,如果你的程序選擇了zh-CN的默認語言,就不能再有zh-CN.xlf的多語言資源,否則會提示錯誤,刪掉重復的即可。你也可以在xlf文件上右鍵發送郵件給朋友,翻譯完了再導入進來。
呼呼,先別管翻譯的准不准,代碼里我們可以這樣初始化貨幣列表了:
AllCurrencyItemList = new List<CurrencyItem>() { new CurrencyItem{Code = "AED", Description = AppResources.AED, Image="flag_united_arab_emirates"}, new CurrencyItem{Code = "ALL", Description = AppResources.ALL, Image="flag_albania"}, //new CurrencyItem{Code = "ANG", Description = AppResources.ANG, Image=""}, new CurrencyItem{Code = "ARS", Description = AppResources.ARS, Image="flag_argentina"}, 。。。。。 }
因為是從資源文件中讀取的貨幣描述,所以在UI會顯示和用戶系統匹配的語言。
未完待續。