上一篇文章已經建立了基本的實體類,並且搞定了多語言的問題,以后在app里用字符串的時候就可以從資源文件中取了。現在繼續進行。
一、添加一個頁面
CurrencyExchanger首頁是一個貨幣兌換的列表,這個列表比較復雜,我們先不管,先從簡單的頁面做起。首先要有一個添加貨幣的頁面,顯示所有可添加的貨幣列表。在WP項目中右鍵添加一個頁面,命名為AddCurrencyPage.xaml。
然后MVVM-Sidekick自動添加了一些東西:除了這個頁面之外,在WP項目的ViewModels目錄中添加了一個AddCurrencyPage_Model.cs文件,在StartUps目錄中添加了一個AddCurrencyPage.cs文件。打開看一下:
public static void ConfigAddCurrencyPage() { ViewModelLocator<AddCurrencyPage_Model> .Instance .Register(context => new AddCurrencyPage_Model()) .GetViewMapper() .MapToDefault<AddCurrencyPage>(); }
看到了吧,所有的View和ViewModel都要進行一下配置才能用。第一次用MVVM-Sidekick的時候我沒裝vs擴展,直接引用的類庫,自己手動建立VM,不知道要進行配置,結果死活綁定不上。問作者才知道有這么個東西,所以一定要裝vs擴展插件才可以享受到這個便利。
二、實現第一個Binding列表
在AddCurrencyPage_Model.cs文件中,通過使用propvm代碼段的方式添加以下兩個屬性:

public string AppName { get { return _AppNameLocator(this).Value; } set { _AppNameLocator(this).SetValueAndTryNotify(value); } } #region Property string AppName Setup protected Property<string> _AppName = new Property<string> { LocatorFunc = _AppNameLocator }; static Func<BindableBase, ValueContainer<string>> _AppNameLocator = RegisterContainerLocator<string>("AppName", model => model.Initialize("AppName", ref model._AppName, ref _AppNameLocator, _AppNameDefaultValueFactory)); static Func<string> _AppNameDefaultValueFactory = () => { return AppResources.AppName; }; #endregion public string PageName { get { return _PageNameLocator(this).Value; } set { _PageNameLocator(this).SetValueAndTryNotify(value); } } #region Property string PageName Setup protected Property<string> _PageName = new Property<string> { LocatorFunc = _PageNameLocator }; static Func<BindableBase, ValueContainer<string>> _PageNameLocator = RegisterContainerLocator<string>("PageName", model => model.Initialize("PageName", ref model._PageName, ref _PageNameLocator, _PageNameDefaultValueFactory)); static Func<string> _PageNameDefaultValueFactory = () => { return AppResources.AddCurrency_PageName; }; #endregion
注意,默認值可以直接返回資源中的本地化字符串:return AppResources.AddCurrency_PageName;
AddCurrency_PageName這個資源是事先在Strings\en-US\Resources.resw資源文件中定義好的,以后就不再詳述了。
把這兩個屬性綁定到頁面標題區域就可以了,這樣可以根據用戶選擇的語言顯示本地化的語言。
<StackPanel Grid.Row="0" Margin="19,0,0,0"> <TextBlock Text="{Binding AppName}" Style="{ThemeResource TitleTextBlockStyle}" Margin="0,12,0,0"/> <TextBlock Text="{Binding PageName}" Margin="0,-6.5,0,26.5" Style="{ThemeResource HeaderTextBlockStyle}" CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}"/> </StackPanel>
然后需要有一個顯示貨幣的列表:

/// <summary> /// 貨幣列表 /// </summary> public ObservableCollection<CurrencyItem> CurrencyItemList { get { return _CurrencyItemListLocator(this).Value; } set { _CurrencyItemListLocator(this).SetValueAndTryNotify(value); } } #region Property ObservableCollection<CurrencyItem> CurrencyItemList Setup protected Property<ObservableCollection<CurrencyItem>> _CurrencyItemList = new Property<ObservableCollection<CurrencyItem>> { LocatorFunc = _CurrencyItemListLocator }; static Func<BindableBase, ValueContainer<ObservableCollection<CurrencyItem>>> _CurrencyItemListLocator = RegisterContainerLocator<ObservableCollection<CurrencyItem>>("CurrencyItemList", model => model.Initialize("CurrencyItemList", ref model._CurrencyItemList, ref _CurrencyItemListLocator, _CurrencyItemListDefaultValueFactory)); static Func<ObservableCollection<CurrencyItem>> _CurrencyItemListDefaultValueFactory = () => { return new ObservableCollection<CurrencyItem>(); }; #endregion
此處需要注意,建議把默認返回值改為new出來的一個列表,不然有時候忘了初始化就使用會報錯。
給列表賦值的方法中哪呢?往下找到被注釋掉的一大坨代碼,Life Time Event Handling這個region里:
///// <summary> ///// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view's ViewModel property ///// </summary> ///// <param name="view">View that firing Load event</param> ///// <returns>Task awaiter</returns> //protected override Task OnBindedViewLoad(MVVMSidekick.Views.IView view) //{ // return base.OnBindedViewLoad(view); //}
嗯貌似就是這個了,頁面載入完成時觸發的事件,可以在這里面對列表賦值,把注釋去掉,代碼加一行:
/// <summary> /// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view's ViewModel property /// </summary> /// <param name="view">View that firing Load event</param> /// <returns>Task awaiter</returns> protected override Task OnBindedViewLoad(MVVMSidekick.Views.IView view) { CurrencyItemList = new ObservableCollection<CurrencyItem>(Context.Instance.AllCurrencyItemList); return base.OnBindedViewLoad(view); }
這樣貨幣列表就有數據了。
現在中頁面中放一個ListView控件來顯示數據,把ItemsSource屬性綁定到貨幣列表CurrencyItemList上:
<Grid Grid.Row="1" x:Name="ContentRoot" Margin="19,9.5,19,0"> <ListView x:Name="list" ItemsSource="{Binding CurrencyItemList}" /> </Grid>
好了怎么來看我們的成果呢?需要從MainPage導航到AddCurrencyPage對吧,對了可以用一個appbar來導航。
三、WP8.1的appbar與導航
WP8以前用的appbar是無法支持綁定的,用着不太方便,WP8.1與Win8.1進行了統一,可以支持Command綁定了。順便說一句,MVVM-Sidekick為WP8的appbar也提供了一種綁定Command的方法,但此處不是重點,略過不表(韋恩卑鄙會大怒的哈哈哈好不容易寫出來了我給略過了)
現在看看Universal App中怎么添加appbar。WP8.1中已經把appbar改為CommandBar了,具體使用方法可見msdn:http://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/hh781232.aspx
我們先來實現導航的Command,可以用propcmd的代碼段快速生成一個Command,在MainPage_Model.cs文件中添加以下代碼:
/// <summary> /// 導航到添加貨幣頁面 /// </summary> public CommandModel<ReactiveCommand, String> CommandNavToAddCurrency { get { return _CommandNavToAddCurrencyLocator(this).Value; } set { _CommandNavToAddCurrencyLocator(this).SetValueAndTryNotify(value); } } #region Property CommandModel<ReactiveCommand, String> CommandNavToAddCurrency Setup protected Property<CommandModel<ReactiveCommand, String>> _CommandNavToAddCurrency = new Property<CommandModel<ReactiveCommand, String>> { LocatorFunc = _CommandNavToAddCurrencyLocator }; static Func<BindableBase, ValueContainer<CommandModel<ReactiveCommand, String>>> _CommandNavToAddCurrencyLocator = RegisterContainerLocator<CommandModel<ReactiveCommand, String>>("CommandNavToAddCurrency", model => model.Initialize("CommandNavToAddCurrency", ref model._CommandNavToAddCurrency, ref _CommandNavToAddCurrencyLocator, _CommandNavToAddCurrencyDefaultValueFactory)); static Func<BindableBase, CommandModel<ReactiveCommand, String>> _CommandNavToAddCurrencyDefaultValueFactory = model => { //var resource = "NavToAddCurrency"; // Command resource var resource = AppResources.AppBarButton_Add; var commandId = "NavToAddCurrency"; var vm = CastToCurrentType(model); var cmd = new ReactiveCommand(canExecute: true) { ViewModel = model }; //New Command Core cmd .DoExecuteUITask( vm, async e => { //Todo: Add NavToAddCurrency logic here, or //await MVVMSidekick.Utilities.TaskExHelper.Yield(); await vm.StageManager.DefaultStage.Show(new AddCurrencyPage_Model()); } ) .DoNotifyDefaultEventRouter(vm, commandId) .Subscribe() .DisposeWith(vm); var cmdmdl = cmd.CreateCommandModel(resource); cmdmdl.ListenToIsUIBusy(model: vm, canExecuteWhenBusy: false); return cmdmdl; }; #endregion
這一大坨代碼里起主要作用的是:
await vm.StageManager.DefaultStage.Show(new AddCurrencyPage_Model());
使用StageManager不但可以實現頁面間的導航,還可以實現頁面某區域的導航,非常方便。
這里來簡單解釋一下,resource是這個command攜帶的一個資源,可以在里面放字符串或任何東西,用於UI的綁定,比如按鈕顯示的文本。我在這里把默認的字符串改為了AppResources.AppBarButton_Add也是為了實現多語言。vm是當前的ViewModel的實例,可以引用VM中的屬性或方法。每個VM都有一個IsUIBusy屬性來標識當前UI是否處於Busy狀態,默認的DoExecuteUIBusyTask方法是向頁面通知這個方法比較費時,頁面可以顯示一個轉圈圈。但據我測試如果在導航的時候使用DoExecuteUIBusyTask,返回的時候會導致原頁面還處於Busy狀態,導致command無法繼續執行,因此把DoExecuteUIBusyTask改為DoExecuteUITask就可以了。
cmdmdl.ListenToIsUIBusy(model: vm, canExecuteWhenBusy: false);這句是讓command監聽頁面的IsUIBusy狀態,如果Busy的時候就不能執行。一般會設置為false,也可以根據需要設置為true,就是不管頁面是否處於Busy狀態都可以繼續執行這個command。
下一步把這個command綁定到按鈕上。
在MainPage.xaml中添加一下代碼,其實只要把注釋部分取消就可以了:
<Page.DataContext> <Binding RelativeSource="{RelativeSource Mode=Self}" Path="ViewModel"/> </Page.DataContext> <mvvm:MVVMPage.ViewModel> <Binding Source="{StaticResource DesignVM}" /> </mvvm:MVVMPage.ViewModel> <mvvm:MVVMPage.BottomAppBar> <CommandBar d:DataContext="{StaticResource DesignVM }"> <AppBarButton Icon="AllApps" Label="{Binding CommandNavToAddCurrency.Resource}" Command="{Binding CommandNavToAddCurrency}" /> </CommandBar> </mvvm:MVVMPage.BottomAppBar>
看到了吧,在Label的屬性我綁定到了CommandNavToAddCurrency.resource,而resource是根據用戶語言顯示的,所以用戶會看到一個本地化的文本。resource里不但可以放文本,還可以放icon圖片的名稱,實現動態改變按鈕圖標的功能。具體實現以后再講。
現在來測試一下,運行程序,點擊appbar,可以顯示到添加貨幣的頁面了。
四、使用Blend設計模板
沒有列表項模板的話只是展示了一堆實體列表,現在需要把model用模板展現出來。這時候就要請Blend出馬了。在WP項目上右鍵選擇使用Blend打開。打開AddCurrencyPage.xaml文件。如果提示vm:AddCurrencyPage_Model不存在之類的錯誤的話,把項目重新編譯一遍再重新打開一般就可以解決了。
使用Blend的時候一定要使用設計時視圖,可以方便的查看當前顯示的是什么樣子,而不用蒙着改。這也是提高工作效率的重點。現在為VM添加設計時支持:
打開AddCurrencyPage_Model.cs文件,默認是沒有構造函數的,添加以下代碼:
public AddCurrencyPage_Model() { if (IsInDesignMode) { Context.Instance.Init(); CurrencyItemList = new ObservableCollection<CurrencyItem>(Context.Instance.AllCurrencyItemList); //AppName = "CurrencyExchanger"; //PageName = "add currency"; } }
IsInDesignMode是標識當前是設計模式,這樣Blend會找到里面的代碼執行,具體到這個頁面就是初始化貨幣列表,這樣就可以中設計模板的時候看到實際數據了。在Blend的listview控件上右鍵-編輯其他模板-編輯生成的項-創建空白項,輸入項模板名稱ItemDataTemplate,Blend會自動添加一個項模板進入模板編輯模式,然后就可以方便的進行綁定了,比如綁定一個TextBlock的Text屬性,先選中,在右側屬性欄Text屬性右側的小方塊點一下,選擇創建數據綁定,就可以看到這個窗口:
選擇Code屬性,就綁定好了。大概做成這個樣子:
具體代碼如下:
<DataTemplate x:Key="ItemDataTemplate"> <Grid Margin="0" > <Grid.ColumnDefinitions> <ColumnDefinition Width="90"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Image HorizontalAlignment="Left" Height="63" VerticalAlignment="Top" Width="90"/> <Grid Height="63" Grid.Column="1" Margin="12,0,0,0"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock TextWrapping="Wrap" Text="{Binding Code}" Style="{StaticResource ListViewItemTextBlockStyle}" Margin="0"/> <TextBlock TextWrapping="Wrap" Text="{Binding Description}" Margin="0" Grid.Row="1" Style="{StaticResource ListViewItemSubheaderTextBlockStyle}" VerticalAlignment="Bottom"/> </Grid> </Grid> </DataTemplate>
列表的XAML代碼變成了:
<ListView x:Name="list" ItemsSource="{Binding CurrencyItemList}" ItemTemplate="{StaticResource ItemDataTemplate}" />
這里圖片還沒綁定,因為我還沒把圖片導進來呢……
五、顯示圖片
把整理好的國旗圖片放到Assets目錄下的Flag目錄中,在貨幣列表的model中已經有了國旗圖片名稱,再轉換一下得到實際地址就可以了。實際上你也可以中貨幣列表初始化的時候直接給他一個完整的地址,就不用再轉換了。這里只是演示一下怎么用converter。你已經會了?嗯那不多說了直接上代碼吧。
因為這個Converter是可以共享的,因此放到Shared項目中。在Utilities目錄中添加一個Converters.cs文件,添加一個類:
public class FlagConverter : IValueConverter { object IValueConverter.Convert(object value, Type targetType, object parameter, string language) { if (value != null) { //也可以設置ImageSource 但不如直接返回一個字符串簡單 //return new BitmapImage(new Uri("/Assets/Flag/" + value.ToString() + ".png", UriKind.Relative)); return "/Assets/Flag/" + value.ToString() + ".png"; } else { //return new BitmapImage(new Uri("/Assets/Images/Flag/flag_white.png", UriKind.Relative)); return "/Assets/Flag/flag_white.png"; } } object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotImplementedException(); } }
然后在頁面中定義這個Converter,不過因為這個轉換器屬於比較常用的,我們也可以直接放到App.xaml里,這樣其他頁面也可以直接用了。
<Application x:Class="CurrencyExchanger.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:CurrencyExchanger" xmlns:utils="using:CurrencyExchanger.Utilities"> <Application.Resources> <utils:FlagConverter x:Key="FlagConverter" /> </Application.Resources> </Application>
現在可以在添加貨幣的頁面中這樣用了,列表模板中圖像這個地方改為:
<Image HorizontalAlignment="Left" Height="63" VerticalAlignment="Top" Width="90" Source="{Binding Image,Converter={StaticResource FlagConverter}}"/>
運行一下試試,圖片出來了:
六、進軍Universal!
WP項目中顯示出了所有的貨幣列表,但Windows項目還沒搞呢。既然是Universal App,一定要多用Shared項目,能否把不同平台的View都綁定到一個ViewModel呢?試試看。
在Windows項目中右鍵添加一個頁面,命名為AddCurrencyPage.xaml,和WP項目中的添加貨幣頁面名稱相同。然后看到Windows項目中也增加了類似的ViewModel文件和vm配置文件。現在把Windows項目中的StartUps\AddCurrencyPage.cs、ViewModels\AddCurrencyPage_Model.cs刪除,把WP項目中的這兩個文件轉移到Shared項目中。
懷着激動的心情運行一下WP項目,嗯很好,跟以前一樣。這樣就可以在Windows項目和WP項目中使用同一個ViewModel了。
七、Windows項目添加appbar
同樣把MainPage.xmal中的appbar注釋部分取消,綁定到Command。
<Page.DataContext> <Binding RelativeSource="{RelativeSource Mode=Self}" Path="ViewModel"/> </Page.DataContext> <mvvm:MVVMPage.ViewModel> <Binding Source="{StaticResource DesignVM}" /> </mvvm:MVVMPage.ViewModel> <mvvm:MVVMPage.BottomAppBar> <CommandBar> <AppBarButton Icon="Add" Label="{Binding CommandNavToAddCurrency.Resource}" Command="{Binding CommandNavToAddCurrency}"/> </CommandBar> </mvvm:MVVMPage.BottomAppBar>
七、使用GridView顯示貨幣列表
Windows Store App布局跟WP不太一樣,以橫向滾動為主,這里使用GridView來展示貨幣列表頁面。用Blend在AddCurrencyPage.xaml中拖一個GridView進來,將其ItemsSource綁定到VM的CurrencyItemList。
然后用Blend設計GridView的項模板。代碼如下:
<DataTemplate x:Key="ItemDataTemplate"> <Grid Margin="0" Width="400" > <Grid.ColumnDefinitions> <ColumnDefinition Width="90"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Image HorizontalAlignment="Left" Height="63" VerticalAlignment="Top" Width="90" Source="{Binding Image, Converter={StaticResource FlagConverter}}"/> <Grid Height="63" Grid.Column="1" Margin="12,0,0,0"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock TextWrapping="Wrap" Text="{Binding Code}" Margin="0"/> <TextBlock TextWrapping="Wrap" Text="{Binding Description}" Margin="0" Grid.Row="1" VerticalAlignment="Bottom"/> </Grid> </Grid> </DataTemplate>
因為是橫向滾動的,最好給Grid設置一個固定的寬度。其他的基本不變。
運行一下Windows項目,?報錯了?
“System.Exception”類型的異常在 CurrencyExchanger.Windows.exe 中發生,但未在用戶代碼中進行處理
WinRT 信息: 未找到 ResourceMap。
其他信息: 未找到 ResourceMap。
原來是找不到資源了。光設置了WP項目的多語言資源,忘了Windows項目了。在Windows項目上右鍵添加翻譯語言,把缺的語言添加上,保持和WP項目一致。為了提升共享代碼程度,把String文件夾也整個移到Shared目錄里,甚至可以把Assets目錄里的Flags圖片都移動過去。
兩個項目分別運行一遍,完全OK。現在可以感受到Universal App的好處了,兩個平台,只用了一個ViewModel,只實現不同的UI即可。