使用MVVM-Sidekick開發Universal App(一)

yan_xiaodi發表於2014-06-21

終於要邁進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-SidekickVS擴充套件外掛,在VS2013update2的工具-擴充套件與更新選單中搜尋mvvm-sidekick就可以找到這個擴充套件,下載安裝即可。安裝後會新增專案模板和程式碼段,比較方便。

githubhttps://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.1WP8.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();
            
        }

這個方法對ViewViewModel進行了配置,以後新加View的話,MVVM-Sidekick會自動新增所需的ViewModel,並在這個類中進行註冊,方便使用。

 

ViewModel資料夾中放著所需的VM,這個資料夾也是在Shared專案中,說明我們可以只用共享的VM去作為不同平臺的ViewDataContext,實現了共享程式碼的目的。

 

然後看MainPage_Model.cs這個vm,這個類繼承了ViewModelBase<MainPage_Model>ViewModelBaseMVVM-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作為GridDataContext,這樣就可以進行繫結了,可以看到有一個TextBlockText屬性繫結到了VMTitle欄位。

 

大體的專案結構就是這樣,下面我們就開始升級。說是升級,其實就是重新開發啊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


    }
View Code

這個Model要繼承BindableBase<CurrencyItem>,在MVVM-SidekickBindableBase是和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

    }
View Code

這個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;
            }
        }
}
View Code

在裡面新增一個列表:

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會顯示和使用者系統匹配的語言。

未完待續。

相關文章