65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MVVM 和 PCL 在 Windows Phone 和 Windows 应用商店应用程序之间共享代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.39/5 (7投票s)

2014年3月31日

Apache

5分钟阅读

viewsIcon

16923

本文解释了开发者如何利用 Model-View-ViewModel (MVVM) 模式和 .NET 可移植类库 (PCL) 来创建可在 Windows Phone 和 Windows 应用商店平台开发的应用程序中共享/重用的代码。

随着业务希望扩大其覆盖范围到不同的设备形态和设备,大量开发者会在不同平台构建原生应用。一个特定的场景是当开发者希望针对 Windows Phone 和 Windows 应用商店平台时。与 Android 不同,开发者必须在 Visual Studio 中为 Windows 应用商店和 Windows Phone 应用创建不同的项目。

本文解释了开发者如何利用 Model-View-ViewModel (MVVM) 和 .NET 可移植类库 在两个平台之间创建通用的共享业务逻辑。本文将不介绍 MVVM 的入门知识,因此我希望读者已经了解它。如果您不了解 MVVM 是什么,我建议在继续阅读之前先阅读一些关于 MVVM 的入门文章。

应用程序

为了简单起见和便于学习,我们将在手机和平板电脑上创建一个简单的联系人表单应用,其外观将类似于下面的草图。

设计

这里的设计目标是将所有核心业务模型、视图模型、助手和存储库移到一个单独的可移植类库中,该库可以被 UI/视图项目引用。视图与视图模型之间的连接将通过数据绑定和命令等方式实现。

值得一提的一点是,PCL 中的助手和存储库将进一步与外部可移植库(例如 Http Client)进行通信,以使用相同的 PCL DLL 在两个平台上执行网络操作。在实际情况中,您可能无法 100% 实现这一点,但在许多场景下是可以实现的。

实现

那么,让我们开始编码吧!我们将采用一种简单而朴素的方法来实现上述设计。

我们将从创建三个项目开始。DemoMvvm.Shared 是一个可移植类库。DemoMvvm.WP 是一个 Windows Phone 项目,而 DemoMvvm.WS 是一个 Windows 应用商店项目。

我们现在将在 DemoMvvm.Shared -> Model 中创建一个新的 "Category" 类模型,用于填充我们联系人页面上的类别。(参见上面的应用原型)。

namespace DemoMvvm.Shared.Model
{
    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    public class Category : INotifyPropertyChanged
    {
        private string _title;

        public event PropertyChangedEventHandler PropertyChanged;

        public int Id { get; set; }

        public string Title
        {
            get
            {
                return this._title;
            }

            set
            {
                this._title = value;
                this.OnPropertyChanged();
            }
        }

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var eventHandler = this.PropertyChanged;
            if (eventHandler != null)
            {
                eventHandler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

该模型包含 Title 和 Id 两个简单的属性。Title 字段用于填充类别名称,它有自己的自定义 getter 和 setter,在为其赋值时会调用 OnPropertyChanged 方法。关于 INotifyPropertyChanged 的简短说明。INotifyPropertyChanged 是框架自带的一个接口,实现它的类能够通知其视图/客户端属性值已更改。因此,一旦我们将视图与 ViewModel/Model 绑定,我们就需要一种机制来通知视图/UI 某个值已更新。当我们在模型中更改属性值时,它会触发属性更改事件,这就是为什么我们要调用“OnPropertyChanged”方法,以告知视图属性值已更改,请更新您的 UI。这是 INotifyPropertyChanged 接口的主要用途,我们需要在所有打算绑定的模型或 ViewModel 中实现它。

让我们为我们的联系人页面创建一个 ViewModel。

public class ContactPageViewModel : INotifyPropertyChanged
{
    private readonly IContactRepository _contactRepository;

    private readonly IValidator _requiredFieldValidator;

    private ObservableCollection<Category> _categories;

    private string _email;

    private string _inquiry;

    private bool _isValid;

    private string _name;

    private Category _selectedCategory;

    public ContactPageViewModel()
    {
        this._requiredFieldValidator = new RequiredFieldValidator();
        this._contactRepository = new ContactRepository();
        this.SubmitCommand = new RelayCommand(this.OnSubmit);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<Category> Categories
    {
        get
        {
            return this._categories;
        }

        set
        {
            this._categories = value;
            this.OnPropertyChanged();
        }
    }

    public string Email
    {
        get
        {
            return this._email;
        }

        set
        {
            this._email = value;
            this.OnPropertyChanged();
        }
    }

    public string Inquiry
    {
        get
        {
            return this._inquiry;
        }

        set
        {
            this._inquiry = value;
            this.OnPropertyChanged();
        }
    }

    public bool IsValid
    {
        get
        {
            return this._isValid;
        }

        set
        {
            this._isValid = value;
            this.OnPropertyChanged();
        }
    }

    public string Name
    {
        get
        {
            return this._name;
        }

        set
        {
            this._name = value;
            this.OnPropertyChanged();
        }
    }

    public Category SelectedCategory
    {
        get
        {
            return this._selectedCategory;
        }

        set
        {
            this._selectedCategory = value;
            this.OnPropertyChanged();
        }
    }

    public ICommand SubmitCommand { get; set; }

    public void InitializeViewModel()
    {
        this.Name = string.Empty;
        this.Email = string.Empty;
        this.Categories = new ObservableCollection<Category>(
            this._contactRepository.PopulateDummyDataInCategories());
        this._requiredFieldValidator.RegisterPropertyChangeForValidation(this);
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private void OnSubmit()
    {
        this._contactRepository.Submit(this.Name, this.Email, this.SelectedCategory.Id, this.Inquiry);
    }
}

视图模型类包含字符串类型的 Name、Email、Inquiry 和 ObservableCollection<Catgory> 类型的 Categories 集合。这些属性将与 UI 字段绑定。请注意,所有这些属性都有自定义的 getter 和 setter,当新值赋给属性时会触发 PropertyChanged 事件。这将让 UI 知道值已更改。我们还有一个名为“SelectedCategory”的属性,用于填充用户选择的类别。

另一个属性是 SubmitCommand,它是 ICommand 类型,并被初始化为在触发时调用 OnSubmit 方法。此属性将用于绑定 UI 上的 Submit 按钮命令。

为简单起见,本文不涉及验证器和存储库,您可以在原始源代码中找到它们。

现在,让我们进入每个平台上的视图。

视图 - Windows 应用商店
<Page
    x:Name="pageRoot"
    x:Class="DemoMvvm.WS.View.ContactPage"
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DemoMvvm.WS.View"
    xmlns:common="using:DemoMvvm.WS.Common"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ChildrenTransitions>
            <TransitionCollection>
                <EntranceThemeTransition/>
            </TransitionCollection>
        </Grid.ChildrenTransitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="140"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- page title -->
        <Grid Grid.Row="0" Grid.Column="1">

            <TextBlock x:Name="pageTitle" Text="Contact Form" Style="{StaticResource HeaderTextBlockStyle}"  
                        IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/>
        </Grid>

        <!-- Content Grid -->
        <Grid Grid.Row="1" Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="100"/>
                <RowDefinition Height="100"/>
                <RowDefinition Height="100"/>
                <RowDefinition Height="200"/>
                <RowDefinition Height="100"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="400"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Grid.Row="0"  Text="Name: " Style="{StaticResource BodyTextBlockStyle}"  VerticalAlignment="Center"/>
            <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Name, Mode=TwoWay}" Height="40"/>
            <TextBlock Grid.Column="0" Grid.Row="1"  Text="Email: " Style="{StaticResource BodyTextBlockStyle}" VerticalAlignment="Center"/>
            <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Email, Mode=TwoWay}" Height="40" />

            <TextBlock Grid.Column="0" Grid.Row="2"  Text="Category: " Style="{StaticResource BodyTextBlockStyle}"  VerticalAlignment="Center"/>
            <ComboBox Grid.Column="1" Grid.Row="2" Height="40" 
                      ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=TwoWay}">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Path=Title}" />
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>

            <TextBlock Grid.Column="0" Grid.Row="3"  Text="Inquiry: " Style="{StaticResource BodyTextBlockStyle}"  VerticalAlignment="Center"/>
            <TextBox  Grid.Column="1" Grid.Row="3" Text="{Binding Path=Inquiry, Mode=TwoWay}"  Height="100" />

            <Button Grid.Column="1" Grid.Row="4" IsEnabled="{Binding IsValid, Mode=OneWay}" Content="Submit" Command="{Binding SubmitCommand}" Width="150" Margin="50,0,0,0" />
            <Button Grid.Column="1" Grid.Row="4" Content="Cancel" Width="150" Margin="200,0,0,0" />
        </Grid>
    </Grid>
</Page>

视图使用两列内容的网格实现,其中包含作为标题/标签的文本块和文本框,这些文本框与视图模型属性进行双向绑定以存储用户数据。

视图 - Windows Phone
<phone:PhoneApplicationPage
    x:Class="DemoMvvm.WP.View.ContactPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d"
    shell:SystemTray.IsVisible="True">

    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="Contact Me" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
            <TextBlock Text="This is a sample page to demostrate MVVM implementation!" Style="{StaticResource PhoneTextNormalStyle}" TextWrapping="Wrap"/>
        </StackPanel>

        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <StackPanel Margin="12,17,0,28">
                <TextBlock Text="Name: " Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=Explicit}" TextChanged="OnTextChangeUpdateSource" />

                <TextBlock  Text="Email: " Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBox Text="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=Explicit}" TextChanged="OnTextChangeUpdateSource" />

                <TextBlock  Text="Category: " Style="{StaticResource PhoneTextNormalStyle}"/>
                <toolkit:ListPicker ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=TwoWay}">
                    <toolkit:ListPicker.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="{Binding Path=Title}" />
                            </StackPanel>
                        </DataTemplate>
                    </toolkit:ListPicker.ItemTemplate>
                </toolkit:ListPicker>

                <TextBlock  Text="Inquiry: " Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBox  Text="{Binding Path=Inquiry, Mode=TwoWay, UpdateSourceTrigger=Explicit}" 
                          TextChanged="OnTextChangeUpdateSource" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Visible" Height="128"/>

                <Button IsEnabled="{Binding IsValid, Mode=OneWay}" Content="Submit" Command="{Binding SubmitCommand}" />
            </StackPanel>
        </Grid>
    </Grid>

</phone:PhoneApplicationPage>

Windows Phone 视图使用简单的堆栈面板来实现,将标签和输入文本框堆叠在一起。同样,文本框与视图模型属性进行双向绑定以存储用户数据。这里的一个小改动是 UpdateSourceTrigger 被设置为 explicit,这意味着我们将手动触发更新绑定源。这是通过在文本框上注册 TextChanged 事件来完成的,该事件将在代码隐藏中更新绑定源。

在代码隐藏 (.cs) 中,我们用 ViewModel 初始化 DataContext 属性,仅此而已。

    public sealed partial class ContactPage : Page
    {
        private readonly ContactPageViewModel _viewModel;

        public ContactPage()
        {
            this.InitializeComponent();
            this._viewModel = new ContactPageViewModel();
            this.DataContext = this._viewModel;
            this.Loaded += this.OnPageLoaded;
        }

        private void OnPageLoaded(object sender, RoutedEventArgs e)
        {
            this._viewModel.InitializeViewModel();
        }
    }

就这样。UI 上的 TextBox 使用特殊的 XAML 语法“{Binding *PropertyName*, Mode = *OneWay/TwoWay/OneTime*}”进行绑定。这里的模式指定绑定是单向(从 Model -> UI)还是双向(UI -> Model 反之亦然)。另一个需要注意的重要事项是 Command。Command 是将视图事件与 ViewModel 连接的方式。在这种情况下,我们将提交按钮与 ViewModel 上的 SubmitCommand 注册,这将最终调用 OnSubmit 操作。

但是 MVVM 的潜力在于如何处理验证。我所做的是,只有当 IsValid 属性为 true 时,提交按钮才会启用。而 IsValid 属性是由 RequiredFieldValidator 更新的。

然而,整个故事的精彩之处在于,我们一次编写模型、视图模型、助手、验证器,并在两个平台之间使用它们。

等等!难道这一切不是拥有共享 DLL 的简单概念吗?答案在一定程度上是肯定的。关键在于 Microsoft 的开发 API 似乎是 .NET,但它们在每个平台上实际上是不同的。因此,可移植类库在这里允许我们共享具有在两个平台都可用的通用 API 的公共功能。此外,现在也有像便携式 Http Client 这样的第三方库,提供相同的 API。因此,如果您探索源代码,我使用了 NuGet 包并添加了便携式 Http Client 的引用,以便使用我们的共享库在两个平台上执行网络操作。

原始完整 **源代码** 可在 GitHub 获取:https://github.com/adilmughal/DemoMvvm-Sharing-WP-WS

© . All rights reserved.