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

上一篇文章已經創建了基本的實體類,而且搞定了多語言的問題,之後在app裏用字符串的時候就能夠從資源文件中取了。如今繼續進行。windows

1、添加一個頁面設計模式

CurrencyExchanger首頁是一個貨幣兌換的列表,這個列表比較複雜,咱們先無論,先從簡單的頁面作起。首先要有一個添加貨幣的頁面,顯示全部可添加的貨幣列表。在WP項目中右鍵添加一個頁面,命名爲AddCurrencyPage.xaml。app

而後MVVM-Sidekick自動添加了一些東西:除了這個頁面以外,在WP項目的ViewModels目錄中添加了一個AddCurrencyPage_Model.cs文件,在StartUps目錄中添加了一個AddCurrencyPage.cs文件。打開看一下:async

public static void ConfigAddCurrencyPage()
        {
            ViewModelLocator<AddCurrencyPage_Model>
                .Instance
                .Register(context =>
                    new AddCurrencyPage_Model())
                .GetViewMapper()
                .MapToDefault<AddCurrencyPage>();

}

 

看到了吧,全部的View和ViewModel都要進行一下配置才能用。第一次用MVVM-Sidekick的時候我沒裝vs擴展,直接引用的類庫,本身手動創建VM,不知道要進行配置,結果死活綁定不上。問做者才知道有這麼個東西,因此必定要裝vs擴展插件才能夠享受到這個便利。mvvm

2、實現第一個Binding列表ide

在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
View Code

 

注意,默認值能夠直接返回資源中的本地化字符串:return AppResources.AddCurrency_PageName;佈局

AddCurrency_PageName這個資源是事先在Strings\en-US\Resources.resw資源文件中定義好的,之後就再也不詳述了。測試

把這兩個屬性綁定到頁面標題區域就能夠了,這樣能夠根據用戶選擇的語言顯示本地化的語言。this

<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
View Code

 

此處須要注意,建議把默認返回值改成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來導航。

3、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,能夠顯示到添加貨幣的頁面了。

4、使用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會找到裏面的代碼執行,具體到這個頁面就是初始化貨幣列表,這樣就能夠中設計模板的時候看到實際數據了。Blendlistview控件上右鍵-編輯其餘模板-編輯生成的項-建立空白項,輸入項模板名稱ItemDataTemplate,Blend會自動添加一個項模板進入模板編輯模式,而後就能夠方便的進行綁定了,好比綁定一個TextBlockText屬性,先選中,在右側屬性欄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}" />

 

這裏圖片還沒綁定,由於我還沒把圖片導進來呢……

5、顯示圖片

把整理好的國旗圖片放到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}}"/>

運行一下試試,圖片出來了:

6、進軍Universal!

WP項目中顯示出了全部的貨幣列表,但Windows項目還沒搞呢。既然是Universal App,必定要多用Shared項目,可否把不一樣平臺的View都綁定到一個ViewModel呢?試試看。

Windows項目中右鍵添加一個頁面,命名爲AddCurrencyPage.xaml,和WP項目中的添加貨幣頁面名稱相同。而後看到Windows項目中也增長了相似的ViewModel文件和vm配置文件。如今把Windows項目中的StartUps\AddCurrencyPage.csViewModels\AddCurrencyPage_Model.cs刪除,把WP項目中的這兩個文件轉移到Shared項目中。

懷着激動的心情運行一下WP項目,嗯很好,跟之前同樣。這樣就能夠在Windows項目和WP項目中使用同一個ViewModel了。

7、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>

7、使用GridView顯示貨幣列表

Windows Store App佈局跟WP不太同樣,以橫向滾動爲主,這裏使用GridView來展現貨幣列表頁面。用BlendAddCurrencyPage.xaml中拖一個GridView進來,將其ItemsSource綁定到VMCurrencyItemList

而後用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便可。

 

相關文章
相關標籤/搜索