如何使用 MVVM 为 Windows Phone 创建本地数据库应用程序

2012/2/9

使用 Windows Phone OS 7.1,您可以将关系数据存储在驻留在应用程序独立存储容器的本地数据库中。本主题介绍如何创建使用本地数据库的多页面待办事项列表应用程序。该应用程序类似于如何为 Windows Phone 创建基本的本地数据库应用程序中推荐的一个应用程序,但是额外还演示了模型视图查看模型 (MVVM) 模式以及如何使用透视控件、Silverlight for Windows Phone 工具包和数据库关联。有关在 Windows Phone 应用程序中使用本地数据库的更多信息,请参阅 Windows Phone 本地数据库概述

提示提示:

本主题与本地数据库示例相对应。若要下载完整的项目,请参阅 Windows Phone 的代码示例

本主题包括以下主要步骤:

  1. 准备应用程序

  2. 创建应用程序 UI

  3. 创建数据模型

  4. 创建 ViewModel

  5. 完成应用程序

下图显示应用程序的数据透视页面和新任务页面。

AP_Con_Local_Database_MVVM

在此过程中,您将修改或创建以下代码文件:

  • MainPage.xaml:修改应用程序的主页以添加透视控件,该控件显示按以下类别分组的待办事项任务:allhomeworkhobbies

  • NewTaskPage.xaml:添加此页面以提供向数据库添加新任务的 UI。此页使用 Silverlight 工具包 ListPicker 控件指定与任务关联的类别。

  • Model\ToDoDataContext.cs:创建此类文件以指定表示本地数据库的 LINQ to SQL 数据上下文和对象模型。对于 MVVM,此类是数据模型。

  • ViewModel\ToDoViewModel.cs:创建此类文件以表示该应用程序的 ViewModel。此类包含几个可观察集合,用于按类别分组待办事项任务。ViewModel 还提供了对数据库执行添加和删除操作的方法。

  • App.xaml.cs:修改此文件以创建可在整个应用程序中访问的本地数据库和静态 ViewModel。

  • MainPage.xaml.cs:修改此页面以将页面的 DataContext 属性设置为应用程序 ViewModel 并处理 adddelete 按键的点按事件。

  • NewTaskPage.xaml.cs:修改此页以将页面的 DataContext 属性设置为应用程序 ViewModel 并处理 okcancel 按键的点按事件。

注意注意:

以下过程中的步骤用于 Visual Studio 2010 Express for Windows Phone。 当您使用用于 Visual Studio 2010 Professional 或 Visual Studio 2010 Ultimate 的插件时,您可能会看到菜单命令或窗口布局中的一些微小改变。

在本节中,将创建应用程序、安装 Silverlight for Windows Phone 工具包、设置程序集引用和添加应用程序使用的图标。

创建应用程序的步骤

  1. 在 Visual Studio 2010 Express for Windows Phone 中,通过选择“文件 | 新建项目”菜单命令创建一个新项目。

  2. 将显示“新建项目”窗口。展开“Visual C#”模板,然后选择“Silverlight for Windows Phone”模板。

  3. 选择“Windows Phone 应用程序”模板。用您选择的名称填写“名称”框。

  4. 单击“确定”。将显示“新建 Windows Phone 应用程序”窗口。

  5. “目标 Windows Phone 版本”菜单中,确保已选择 Windows Phone 7.1。

  6. 单击“确定”。将创建一个新的项目,并且“MainPage.xaml”将在 Visual Studio 设计器窗口中打开。

安装 Silverlight for Windows Phone 工具包

  1. Silverlight for Windows Phone 工具包未包括在 Windows Phone SDK 7.1 中。您必须从 Codeplex 上的 Silverlight 工具包网站中单独下载该工具包。导航到该网站并单击指向 Silverlight for Windows Phone 工具包的链接。

  2. 从 Silverlight for Windows Phone 工具包网页中,单击带有 .MSI 文件扩展名的链接并安装该工具包。

  3. 在安装工具包之后,请记下工具包二进制文件的路径。在 Windows 7 中,可以在 Windows 资源管理器中通过单击“开始”“所有程序”“Microsoft Silverlight for Windows Phone 工具包”“二进制文件”来查找该文件夹。

设置程序集引用

  1. 在 Visual Studio 中,将 Silverlight for Windows Phone 工具包程序集添加到您的项目。从“项目”菜单中单击“添加引用”

  2. “添加引用”窗口中,单击“浏览”选项卡,然后导航到 Silverlight for Windows Phone 工具包二进制文件所在的文件夹。

  3. 选择名为“Microsoft.Phone.Controls.Toolkit.dll”的程序集,然后单击“确定”。在“解决方案资源管理器”中,“Microsoft.Phone.Controls.Toolkit”程序集将显示在“引用”文件夹中。

  4. 该应用程序需要引用适用于 Windows Phone 的 LINQ to SQL 程序集。从“项目”菜单中,单击“添加引用”,从“.NET”选项卡中选择“System.Data.Linq”,然后单击“确定”

  5. 若要使用透视控件,该应用程序需要引用 Windows Phone 控件程序集。从“项目”菜单中,单击“添加引用”,从“.NET”选项卡中选择“Microsoft.Phone.Controls”,然后单击“确定”

添加应用程序所使用的图标的步骤

  1. “解决方案资源管理器”中,右键单击项目,选择“添加”,然后选择“新文件夹”

  2. 将新文件夹命名为 Images

  3. “解决方案资源管理器”中,右键单击 Images 文件夹,并选择“添加”,然后选择“现有项”。这将打开“添加现有项”菜单,从中您可以选择应用程序使用的图标。

  4. “添加现有项”窗口中,导航到以下路径之一以选择这些图标。此步骤假定采用的是以默认方式安装的 Visual Studio。如果您将其安装在其他位置,则在相应的位置查找图标。

    • 64 位操作系统C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Icons\dark

    • 32 位操作系统C:\Program Files\Microsoft SDKs\Windows Phone\v7.1\Icons\dark

    从该文件夹中,选择以下图标:

    • appbar.add.rest.png

    • appbar.cancel.rest.png

    • appbar.check.rest.png

    • appbar.delete.rest.png

    这些图标是专门用深色背景设计的并且颜色为白色;在白色的“添加现有项”窗口背景上图标可能显示为空白。

  5. “解决方案资源管理器”中,右键单击每个图标并设置文件属性,以便将图标生成为“内容”,并始终复制到输出目录(“始终复制”)。

在本节中,将为该应用程序的主页和新任务页创建 UI。对于 MVVM,该应用程序页面为视图。主页演示如何使用 Pivot 控件显示数据。新任务页演示如何使用 Silverlight for Windows Phone 工具包中的 ListPicker 控件选择数据。

创建主页 UI

  1. 在应用程序的主页 MainPage.xaml 中,向位于页面顶部的 phone:PhoneApplicationPage 元素添加以下属性。

    xmlns:controls="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls"
    

    在使用 Pivot 控件时需要该命名空间。

  2. MainPage.xaml 中名为 LayoutRoot 的网格上方,添加以下资源元素。

        <phone:PhoneApplicationPage.Resources>
            <DataTemplate x:Key="ToDoListBoxItemTemplate">
    
                <Grid HorizontalAlignment="Stretch" Width="420">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="100" />
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="100" />
                    </Grid.ColumnDefinitions>
    
                    <CheckBox 
                        IsChecked="{Binding IsComplete, Mode=TwoWay}" 
                        Grid.Column="0" VerticalAlignment="Top"/>
    
                    <TextBlock 
                        Text="{Binding ItemName}" 
                        FontSize="{StaticResource PhoneFontSizeLarge}" 
                        Grid.Column="1" Grid.ColumnSpan="2" 
                        VerticalAlignment="Top" Margin="-36, 12, 0, 0"/>
    
                    <Button                                
                        Grid.Column="3"
                        x:Name="deleteTaskButton"
                        BorderThickness="0"                                                                  
                        Margin="0, -18, 0, 0"
                        Click="deleteTaskButton_Click">
    
                        <Image 
                        Source="/Images/appbar.delete.rest.png"
                        Height="75"
                        Width="75"/>
    
                    </Button>
                </Grid>
            </DataTemplate>
        </phone:PhoneApplicationPage.Resources>
    
    

    每个透视页面都可重复使用此项目模板来显示本地数据库中的一行数据,即一个待办事项任务。每行都包括一个将任务标记为已完成的 CheckBox、一个显示待办事项任务文本的 TextBlock 和一个允许删除任务的 Button。使用对任务的 IsCompleted 属性的双向绑定配置 CheckBox。当您在 UI 中关闭任务时,ViewModel 中任务绑定到的相应可观察集合将自动捕获新值。Textblock 绑定到任务的 ItemName 属性。

  3. “MainPage.xaml”中,使用以下代码替换名为 LayoutRoot 的网格。该代码应该添加在上一步骤中添加的资源元素的下面。

        <!--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 x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
                <TextBlock 
                    x:Name="ApplicationTitle" 
                    Text="LOCAL DATABASE EXAMPLE: TO-DO LIST" 
                    Style="{StaticResource PhoneTextNormalStyle}"/>
            </StackPanel>
    
            <!--ContentPanel - place additional content here.-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <controls:Pivot Margin="0, -36, 0, 0">
                    
                    <controls:PivotItem Header="all">
                        <ListBox 
                            x:Name="allToDoItemsListBox" 
                            ItemsSource="{Binding AllToDoItems}" 
                            Margin="12, 0, 12, 0" Width="440" 
                            ItemTemplate="{StaticResource ToDoListBoxItemTemplate}" />
                    </controls:PivotItem>
                    
                    <controls:PivotItem Header="home">
                        <ListBox 
                            x:Name="homeToDoItemsListBox" 
                            ItemsSource="{Binding HomeToDoItems}" 
                            Margin="12, 0, 12, 0" Width="440" 
                            ItemTemplate="{StaticResource ToDoListBoxItemTemplate}" />
                    </controls:PivotItem>
                    
                    <controls:PivotItem Header="work">
                        <ListBox 
                            x:Name="workToDoItemsListBox" 
                            ItemsSource="{Binding WorkToDoItems}" 
                            Margin="12, 0, 12, 0" Width="440" 
                            ItemTemplate="{StaticResource ToDoListBoxItemTemplate}" />
                    </controls:PivotItem>
                    
                    <controls:PivotItem Header="hobbies">
                        <ListBox
                            x:Name="hobbiesToDoItemsListBox" 
                            ItemsSource="{Binding HobbiesToDoItems}" 
                            Margin="12, 0, 12, 0" Width="440" 
                            ItemTemplate="{StaticResource ToDoListBoxItemTemplate}" />
                    </controls:PivotItem>
                    
                </controls:Pivot>
            </Grid>
        </Grid>
    
    

    此网格指定应用程序标题和 Pivot 控件。透视控件包含以下四个不同的页面:allhomeworkhobbies,分别与可分配给待办事项任务的四个可能类别相对应。每个页面都分别绑定到 ViewModel 中的 AllToDoItemsHomeToDoItemsWorkToDoItemsHobbiesToDoItems 可观察集合。

  4. MainPage.xaml 中,将以下代码添加到名为 LayoutRoot 的网格下面。

        <phone:PhoneApplicationPage.ApplicationBar>
            <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
                
                <shell:ApplicationBarIconButton 
                    IconUri="/Images/appbar.add.rest.png" 
                    Text="add" 
                    x:Name="newTaskAppBarButton" 
                    Click="newTaskAppBarButton_Click"/>
                
            </shell:ApplicationBar>
        </phone:PhoneApplicationPage.ApplicationBar>
    
    

    本页使用应用程序栏显示 add 按键,用于将待办事项任务添加到该数据库。在本主题的后文将创建与此按键相对应的方法。

创建新任务页 UI

  1. “解决方案资源管理器”中,右键单击您的项目,选择“添加”,然后选择“新项”

  2. “添加新项”窗口中,选择“Windows Phone 纵向页面”并将文件命名为 NewTaskPage.xaml。然后单击“添加”

  3. NewTaskPage.xaml 中,向位于页面顶部的 phone:PhoneApplicationPage 元素添加以下属性。

    xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
    

    在使用 Silverlight for Windows Phone 工具包 ListPicker 控件时需要该命名空间。

  4. NewTaskPage.xaml 中,使用以下代码替换名为 LayoutRoot 的网格。

        <!--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 x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
                <TextBlock 
                    x:Name="ApplicationTitle" 
                    Text="LOCAL DATABASE EXAMPLE: TO-DO LIST" 
                    Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock 
                    x:Name="PageTitle" 
                    Text="new task" 
                    Margin="9,-7,0,0" 
                    Style="{StaticResource PhoneTextTitle1Style}"/>
            </StackPanel>
    
            <!--ContentPanel - place additional content here.-->
            <StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <TextBlock Text="Name"/>
                <TextBox x:Name="newTaskNameTextBox"/>
                <TextBlock Text="Category"/>
    
                <toolkit:ListPicker
                    x:Name="categoriesListPicker"
                    ItemsSource="{Binding CategoriesList}"
                    DisplayMemberPath="Name">
                </toolkit:ListPicker>
            </StackPanel>
        </Grid>
    
    

    此网格包含两个 StackPanel 控件。第一个控件指定应用程序和页面标题。第二个控件 StackPanel 包含以下数据输入控件:TextBox 用于输入新待办事项任务文本,ListPicker 控件用于指定该任务类别的 ListPicker 控件。ListPicker 绑定到 CategoriesList,后者是 List 的一个 ViewModel 属性。

  5. NewTaskPage.xaml 中,向名为 LayoutRoot 的网格下添加以下代码。

        <phone:PhoneApplicationPage.ApplicationBar>
            <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
                
                <shell:ApplicationBarIconButton 
                    x:Name="appBarOkButton" 
                    IconUri="/Images/appbar.check.rest.png" 
                    Text="ok" 
                    Click="appBarOkButton_Click"/>
                
                <shell:ApplicationBarIconButton 
                    x:Name="appBarCancelButton" 
                    IconUri="/Images/appbar.cancel.rest.png" 
                    Text="cancel" 
                    Click="appBarCancelButton_Click"/>
                
            </shell:ApplicationBar>
        </phone:PhoneApplicationPage.ApplicationBar>
    
    

    此页面使用应用程序栏显示 okcancel 按键。在本主题的后文将创建与这些按键相对应的方法。

在本节中,将创建 LINQ to SQL 数据上下文以及表示数据库表和关联的对象。首先,将创建文件并为每个表添加模板。然后,将扩建每个表并创建数据上下文。

准备数据模型文件

  1. “解决方案资源管理器”中,右键单击项目,选择“添加”,然后选择“新文件夹”

  2. 将新文件夹命名为 Model

  3. “解决方案资源管理器”中,右键单击 Model 文件夹,选择“添加”,然后选择“新项”

  4. “添加新项”窗口中,选择“代码文件”并将文件命名为 ToDoDataContext.cs。然后单击“添加”

  5. ToDoDataContext.cs 中,添加以下将包含数据模型类的指令和命名空间。

    using System;
    using System.ComponentModel;
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    
    namespace LocalDatabaseSample.Model
    {
    
    }
    
    
  6. ToDoDataContext.cs 中,将以下代码向 LocalDatabaseSample.Model 命名空间添加两次。在以下步骤中,将针对每个表对这些类进行重命名。

    注意注意:

    在以下步骤中重命名这些类之前,Visual Studio 将显示多义性错误。

        [Table]
        public class AddTableNameHere : INotifyPropertyChanged, INotifyPropertyChanging
        {
    
            //
            // TODO: Add columns and associations, as applicable, here.
            //
    
            // Version column aids update performance.
            [Column(IsVersion = true)]
            private Binary _version;
    
            #region INotifyPropertyChanged Members
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            // Used to notify that a property changed
            private void NotifyPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
    
            #endregion
    
            #region INotifyPropertyChanging Members
    
            public event PropertyChangingEventHandler PropertyChanging;
    
            // Used to notify that a property is about to change
            private void NotifyPropertyChanging(string propertyName)
            {
                if (PropertyChanging != null)
                {
                    PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
                }
            }
    
            #endregion
        }
    
    
    

    这是实体的基本模板,即表示本地数据库表的类。为了演示下面的、建议大多数实体所共有的代码功能,此时还缺少列和关联:

    • [table] 属性指定该类将表示一个数据库表。

    • INotifyPropertyChanged 接口用于更改跟踪。

    • INotifyPropertyChanging 接口帮助限制与更改跟踪相关的内存占用。

    • Binary 版本列(具有 [Column(IsVersion = true)] 属性)显著改进表的更新性能。

    注意注意:

    实体可以从其他实体中继承 CLR 对象成员,如事件、方法和属性。无法继承其他实体中的 LINQ to SQL 属性。例如,您可以创建名为 L2SEntity 的基类,该类实现 INotifyPropertyChangedINotifyPropertyChanging。其他实体可以继承 L2SEntity 中的事件和方法,但是 LINQ to SQL 将只识别使用 [Table] 属性显式标记的那些实体。写入 L2SEntity 类本身的任何 LINQ to SQL 属性将不会被其他实体继承。

  7. 将其中一个 AddTableNameHere 类重命名为 ToDoItem。此类将存储待办事项任务信息。

  8. 将其他 AddTableNameHere 类重命名为 ToDoCategory。此类将存储类别列表。

扩建 ToDoItem 类

  1. ToDoDataContext.cs 中,将以下代码添加到 ToDoItem 类。

            // Define ID: private field, public property, and database column.
            private int _toDoItemId;
    
            [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity", CanBeNull = false, AutoSync = AutoSync.OnInsert)]
            public int ToDoItemId
            {
                get {return _toDoItemId;}
                set
                {
                    if (_toDoItemId != value)
                    {
                        NotifyPropertyChanging("ToDoItemId");
                        _toDoItemId = value;
                        NotifyPropertyChanged("ToDoItemId");
                    }
                }
            }
    
            // Define item name: private field, public property, and database column.
            private string _itemName;
    
            [Column]
            public string ItemName
            {
                get {return _itemName;}
                set
                {
                    if (_itemName != value)
                    {
                        NotifyPropertyChanging("ItemName");
                        _itemName = value;
                        NotifyPropertyChanged("ItemName");
                    }
                }
            }
    
            // Define completion value: private field, public property, and database column.
            private bool _isComplete;
    
            [Column]
            public bool IsComplete
            {
                get {return _isComplete;}
                set
                {
                    if (_isComplete != value)
                    {
                        NotifyPropertyChanging("IsComplete");
                        _isComplete = value;
                        NotifyPropertyChanged("IsComplete");
                    }
                }
            }
    
    

    这些字段和属性向该数据库添加以下三列:ToDoItemIdItemNameIsCompleted。[Column] 属性标识该属性表示数据库列的 LINQ to SQL 运行时。有关有效列属性设置的完整列表,请参阅 System.Data.Linq.Mapping.ColumnAttribute

  2. ToDoDataContext.cs 中,将以下代码添加到 ToDoItem 类。

            // Internal column for the associated ToDoCategory ID value
            [Column]
            internal int _categoryId;
    
            // Entity reference, to identify the ToDoCategory "storage" table
            private EntityRef<ToDoCategory> _category;
    
            // Association, to describe the relationship between this key and that "storage" table
            [Association(Storage = "_category", ThisKey = "_categoryId", OtherKey = "Id", IsForeignKey = true)]
            public ToDoCategory Category
            {
                get {return _category.Entity;}
                set
                {
                    NotifyPropertyChanging("Category");
                    _category.Entity = value;
    
                    if (value != null)
                    {
                        _categoryId = value.Id;
                    }
    
                    NotifyPropertyChanging("Category");
                }
            }
    
    

    此代码定义 ToDoItemToDoCategory 表之间的关联。私有 _categoryId 字段存储与待办事项相应的类别标识符。_category 实体引用标识与此表关联的其他表。Category 关联在 get 期间处理检索相应的 ToDoCategory 对象,并在 set 期间将相应的 ToDoCategory 标识符值分配到 _categoryId

    同时,私有实体引用和公有访问器提供了一种使用点表示法在关联中导航的方法。他们还确保当做出对象级别更改时,保持数据库关系是最新的。

扩建 ToDoCategory 类

  1. ToDoDataContext.cs 中,将以下代码添加到 ToDoCategory 类。

            // Define ID: private field, public property, and database column.
            private int _id;
    
            [Column(DbType = "INT NOT NULL IDENTITY", IsDbGenerated = true, IsPrimaryKey = true)]
            public int Id
            {
                get {return _id;}
                set
                {
                    NotifyPropertyChanging("Id");
                    _id = value;
                    NotifyPropertyChanged("Id");
                }
            }
    
            // Define category name: private field, public property, and database column.
            private string _name;
    
            [Column]
            public string Name
            {
                get {return _name;}
                set
                {
                    NotifyPropertyChanging("Name");
                    _name = value;
                    NotifyPropertyChanged("Name");
                }
            }
    
    

    此代码定义类别的 IdName 属性。

  2. ToDoDataContext.cs 中,将以下代码添加到 ToDoCategory 类。

            // Define the entity set for the collection side of the relationship.
            private EntitySet<ToDoItem> _todos;
    
            [Association(Storage = "_todos", OtherKey = "_categoryId", ThisKey = "Id")]
            public EntitySet<ToDoItem> ToDos
            {
                get { return this._todos; }
                set { this._todos.Assign(value); }
            }
    
    
            // Assign handlers for the add and remove operations, respectively.
            public ToDoCategory()
            {
                _todos = new EntitySet<ToDoItem>(
                    new Action<ToDoItem>(this.attach_ToDo), 
                    new Action<ToDoItem>(this.detach_ToDo)
                    );
            }
    
            // Called during an add operation
            private void attach_ToDo(ToDoItem toDo)
            {
                NotifyPropertyChanging("ToDoItem");
                toDo.Category = this;
            }
    
            // Called during a remove operation
            private void detach_ToDo(ToDoItem toDo)
            {
                NotifyPropertyChanging("ToDoItem");
                toDo.Category = null;
            }
    
    

    此代码定义与 ToDoItems 表关联的集合端。EntitySet 构造函数执行 set 上添加事件和删除事件的委托。这有助于对添加到集合的新 ToDoItem 对象正确设置类别信息,即使创建新 ToDoItem 对象时未对其显式设置类别。

创建 LINQ to SQL 数据上下文

  • ToDoDataContext.cs 中,将以下代码添加到 LocalDatabaseSample.Model 命名空间。

        public class ToDoDataContext : DataContext
        {
            // Pass the connection string to the base class.
            public ToDoDataContext(string connectionString) : base(connectionString)
            { }
    
            // Specify a table for the to-do items.
            public Table<ToDoItem> Items;
    
            // Specify a table for the categories.
            public Table<ToDoCategory> Categories;
        }
    
    

    该数据上下文指定两个表:ItemsCategories。Items 表将存储待办事项任务项,其中每一项都基于 ToDoItem 类。Categories 表将存储待办事项任务类别,其中每一个类别都基于 ToDoCategory 类。

在本节中,将创建应用程序的 ViewModel。ViewModel 负责对数据库执行操作并通过几个可观察集合和一个列表将数据显示到应用程序页面。

准备 ViewModel 文件

  1. “解决方案资源管理器”中,右键单击项目,选择“添加”,然后选择“新文件夹”

  2. 将新文件夹命名为 ViewModel

  3. “解决方案资源管理器”中,右键单击 ViewModel 文件夹,选择“添加”,然后选择“新项”

  4. “添加新项”窗口中,选择“代码文件”并将文件命名为 ToDoViewModel.cs。然后单击“添加”

  5. ToDoViewModel.cs 中,添加以下代码。

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Linq;
    
    // Directive for the data model.
    using LocalDatabaseSample.Model;
    
    
    namespace LocalDatabaseSample.ViewModel
    {
        public class ToDoViewModel : INotifyPropertyChanged
        {
            // LINQ to SQL data context for the local database.
            private ToDoDataContext toDoDB;
    
            // Class constructor, create the data context object.
            public ToDoViewModel(string toDoDBConnectionString)
            {
                toDoDB = new ToDoDataContext(toDoDBConnectionString);
            }
    
            //
            // TODO: Add collections, list, and methods here.
            //
    
            // Write changes in the data context to the database.
            public void SaveChangesToDB()
            {
                toDoDB.SubmitChanges();
            }
    
            #region INotifyPropertyChanged Members
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            // Used to notify Silverlight that a property has changed.
            private void NotifyPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            #endregion
        }
    }
    
    

    这是 ViewModel 的模板。它使用 LocalDatabaseSample.Model 命名空间指令引用在上一部分中创建的 LINQ to SQL 数据模型。ViewModel 实现了用于更改跟踪的 INotifyPropertyChanged 接口。

    对本地数据库执行操作是 ViewModel 的核心功能。LINQ to SQL 数据上下文 toDoDB 将在整个 ViewModel 中引用,并在 ViewModel 的构造函数中进行创建。SaveChangesToDB 方法提供了一个将机制保存到视图的通用方法。

创建可观察集合和列表

  • ToDoViewModel.cs 中,将以下代码添加到 ToDoViewModel 类。

            // All to-do items.
            private ObservableCollection<ToDoItem> _allToDoItems;
            public ObservableCollection<ToDoItem> AllToDoItems
            {
                get { return _allToDoItems; }
                set
                {
                    _allToDoItems = value;
                    NotifyPropertyChanged("AllToDoItems");
                }
            }
    
            // To-do items associated with the home category.
            private ObservableCollection<ToDoItem> _homeToDoItems;
            public ObservableCollection<ToDoItem> HomeToDoItems
            {
                get { return _homeToDoItems; }
                set
                {
                    _homeToDoItems = value;
                    NotifyPropertyChanged("HomeToDoItems");
                }
            }
    
            // To-do items associated with the work category.
            private ObservableCollection<ToDoItem> _workToDoItems;
            public ObservableCollection<ToDoItem> WorkToDoItems
            {
                get { return _workToDoItems; }
                set
                {
                    _workToDoItems = value;
                    NotifyPropertyChanged("WorkToDoItems");
                }
            }
    
            // To-do items associated with the hobbies category.
            private ObservableCollection<ToDoItem> _hobbiesToDoItems;
            public ObservableCollection<ToDoItem> HobbiesToDoItems
            {
                get { return _hobbiesToDoItems; }
                set
                {
                    _hobbiesToDoItems = value;
                    NotifyPropertyChanged("HobbiesToDoItems");
                }
            }
    
            // A list of all categories, used by the add task page.
            private List<ToDoCategory> _categoriesList;
            public List<ToDoCategory> CategoriesList
            {
                get { return _categoriesList; }
                set
                {
                    _categoriesList = value;
                    NotifyPropertyChanged("CategoriesList");
                }
            }
    
    

    此代码指定主页中 Pivot 控件使用的可观察集合:AllToDoItemsHomeToDoItemsWorkToDoItemsHobbiesToDoItems。它还指定新任务页上 ListPicker 控件使用的 CategoriesList 列表。

加载集合和列表

  • ToDoViewModel.cs 中,将以下代码添加到 ToDoViewModel 类。

        // Query database and load the collections and list used by the pivot pages.
        public void LoadCollectionsFromDatabase()
        {
    
            // Specify the query for all to-do items in the database.
            var toDoItemsInDB = from ToDoItem todo in toDoDB.Items
                                select todo;
    
            // Query the database and load all to-do items.
            AllToDoItems = new ObservableCollection<ToDoItem>(toDoItemsInDB);
    
            // Specify the query for all categories in the database.
            var toDoCategoriesInDB = from ToDoCategory category in toDoDB.Categories
                                    select category;
    
                
            // Query the database and load all associated items to their respective collections.
            foreach (ToDoCategory category in toDoCategoriesInDB)
            {
                switch (category.Name)
                {
                    case "Home":
                        HomeToDoItems = new ObservableCollection<ToDoItem>(category.ToDos);
                        break;
                    case "Work":
                        WorkToDoItems = new ObservableCollection<ToDoItem>(category.ToDos);
                        break;
                    case "Hobbies":
                        HobbiesToDoItems = new ObservableCollection<ToDoItem>(category.ToDos);
                        break;
                    default:
                        break;
                }
            }
    
            // Load a list of all categories.
            CategoriesList = toDoDB.Categories.ToList(); 
    
        }
    
    

    在此代码中,ViewModel 会加载包含本地数据库中数据的集合和列表。由于延迟加载,直到实例化可观察集合之后,应用程序才会实际查询该数据库。

    注意注意:

    将显示 foreachswitch 语句,演示如何利用 EntitySet 对象检索与该特定类别关联的所有 ToDoItem 对象。在此特定方法中,三个标准 LINQ 查询还可能获得相同的结果。

对数据库执行添加和删除操作

  1. ToDoViewModel.cs 中,将以下代码添加到 ToDoViewModel 类。

            // Add a to-do item to the database and collections.
            public void AddToDoItem(ToDoItem newToDoItem)
            {
                // Add a to-do item to the data context.
                toDoDB.Items.InsertOnSubmit(newToDoItem);
    
                // Save changes to the database.
                toDoDB.SubmitChanges();
    
                // Add a to-do item to the "all" observable collection.
                AllToDoItems.Add(newToDoItem);
    
                // Add a to-do item to the appropriate filtered collection.
                switch (newToDoItem.Category.Name)
                {
                    case "Home":
                        HomeToDoItems.Add(newToDoItem);
                        break;
                    case "Work":
                        WorkToDoItems.Add(newToDoItem);
                        break;
                    case "Hobbies":
                        HobbiesToDoItems.Add(newToDoItem);
                        break;
                    default:
                        break;
                }
            }
    
    

    调用此方法是为了向应用程序中添加任务和新的待办事项。添加待办事项后,需要更新一些内容。首先,更新 LINQ to SQL 数据上下文 toDoDB。然后调用 SubmitChanges 方法将更改保存到数据库。最后,通过调用 Add 方法更新适用的可观察集合。

  2. ToDoViewModel.cs 中,将以下代码添加到 ToDoViewModel 类。

            // Remove a to-do task item from the database and collections.
            public void DeleteToDoItem(ToDoItem toDoForDelete)
            {
    
                // Remove the to-do item from the "all" observable collection.
                AllToDoItems.Remove(toDoForDelete);
    
                // Remove the to-do item from the data context.
                toDoDB.Items.DeleteOnSubmit(toDoForDelete);
    
                // Remove the to-do item from the appropriate category.   
                switch (toDoForDelete.Category.Name)
                {
                    case "Home":
                        HomeToDoItems.Remove(toDoForDelete);
                        break;
                    case "Work":
                        WorkToDoItems.Remove(toDoForDelete);
                        break;
                    case "Hobbies":
                        HobbiesToDoItems.Remove(toDoForDelete);
                        break;
                    default:
                        break;
                }
    
                // Save changes to the database.
                toDoDB.SubmitChanges();                
            }
    
    

    调用此方法是为了从该应用程序中删除某个待办事项和任务。删除项目的顺序与添加项目有点相反。首先,Remove 方法从 AllToDoItems 可观察集合中删除该项。接着,DeleteOnSubmit 方法从 LINQ to SQL 数据上下文删除该项。然后,从相应的经过筛选的集合中删除该项。最后,使用 SubmitChanges 方法从本地数据库中删除该项(并将任何其他更改保存到本地数据库中)。

在本节中,将连接在前几节中创建的所有内容。首先,修改应用程序类以指定静态 ViewModel 和创建本地数据库。然后,修改这些页面的代码隐藏文件以使用 ViewModel 和处理按键点按事件。

完成 App 类

  1. App.xaml.cs 文件中,将以下指令添加到页面顶部。

    // Directives
    using LocalDatabaseSample.Model;
    using LocalDatabaseSample.ViewModel;
    
    
  2. App.xaml.cs 文件中,在 App 类的类构造函数上方添加以下代码。

            // The static ViewModel, to be used across the application.
            private static ToDoViewModel viewModel;
            public static ToDoViewModel ViewModel
            {
                get { return viewModel; }
            }
    
    

    此代码指定一个每个页面都可以在整个应用程序中使用的静态 ViewModel。

  3. App.xaml.cs 文件中,在 App 类的类构造函数最末尾添加以下代码。

        // Specify the local database connection string.
        string DBConnectionString = "Data Source=isostore:/ToDo.sdf";
    
        // Create the database if it does not exist.
        using (ToDoDataContext db = new ToDoDataContext(DBConnectionString))
        {
            if (db.DatabaseExists() == false)
            {
                // Create the local database.
                db.CreateDatabase();
    
                // Prepopulate the categories.
                db.Categories.InsertOnSubmit(new ToDoCategory { Name = "Home" });
                db.Categories.InsertOnSubmit(new ToDoCategory { Name = "Work" });
                db.Categories.InsertOnSubmit(new ToDoCategory { Name = "Hobbies" });
    
                // Save categories to the database.
                db.SubmitChanges();
            }
        }
    
        // Create the ViewModel object.
        viewModel = new ToDoViewModel(DBConnectionString);
    
        // Query the local database and load observable collections.
        viewModel.LoadCollectionsFromDatabase();
    
    

    实例化应用程序对象时,如果本地数据库尚不存在,则创建本地数据库。创建该数据库时,将使用三个类别预填充 ToDoCategory 表。在该数据库存在后,即会使用数据创建并加载 ViewModel 对象。

完成 MainPage 类

  1. 在主页的代码隐藏文件 MainPage.xaml.cs 中,添加以下指令。

    // Directive for the ViewModel.
    using LocalDatabaseSample.Model;
    
    
  2. MainPage.xaml.cs 中,在 MainPage 构造函数中的 InitializeComponent 方法调用之后,添加以下代码。

                // Set the page DataContext property to the ViewModel.
                this.DataContext = App.ViewModel;
    
    

    此代码将页面的 DataContext 属性设置为 ViewModel。DataContext 页面与 LINQ to SQL 数据上下文明显不同。就 MVVM 而言,前者是 View 属性,后者定义 Model。

  3. “MainPage.xaml.cs”中,向 MainPage 类中添加以下代码。

            private void newTaskAppBarButton_Click(object sender, EventArgs e)
            {
                NavigationService.Navigate(new Uri("/NewTaskPage.xaml", UriKind.Relative));
            }
    
    
            private void deleteTaskButton_Click(object sender, RoutedEventArgs e)
            {
                // Cast the parameter as a button.
                var button = sender as Button;
    
                if (button != null)
                {
                    // Get a handle for the to-do item bound to the button.
                    ToDoItem toDoForDelete = button.DataContext as ToDoItem;
    
                    App.ViewModel.DeleteToDoItem(toDoForDelete);
                }
    
                // Put the focus back to the main page.
                this.Focus();
            }
    
            protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
            {
                // Save changes to the database.
                App.ViewModel.SaveChangesToDB();    
            }
    
    

    此代码显示该页面的事件处理代码。点按新任务按键时,使用 NavigationService 对象导航到新任务页面。当调用删除按键时,将检索相应的 ToDoItem 对象并将其发送到 ViewModel 上的 DeleteToDoItem 方法。在用户导航出该页面,数据上下文中的更改都会使用 SaveChangesToDB 方法自动保存到本地数据库。

完成 NewTaskPage 类

  1. 在新任务页面的代码隐藏文件 NewTaskPage.xaml.cs 中,添加以下指令。

    // Directive for the data model.
    using LocalDatabaseSample.Model;
    
    
  2. NewTaskPage.xaml.cs 中,在 NewTaskPage 构造函数的 InitializeComponent 方法调用之后,添加以下代码。

                // Set the page DataContext property to the ViewModel.
                this.DataContext = App.ViewModel;
    
    

    此代码将页面的 DataContext 属性设置为 ViewModel。DataContext 页面与 LINQ to SQL 数据上下文明显不同。就 MVVM 而言,前者是 View 属性,后者定义 Model。

  3. NewTaskPage.xaml.cs 中,向 NewTaskPage 类添加以下代码。

            private void appBarOkButton_Click(object sender, EventArgs e)
            {
                // Confirm there is some text in the text box.
                if (newTaskNameTextBox.Text.Length > 0)
                {
                    // Create a new to-do item.
                    ToDoItem newToDoItem = new ToDoItem
                    {
                        ItemName = newTaskNameTextBox.Text,
                        Category = (ToDoCategory)categoriesListPicker.SelectedItem
                    };
    
                    // Add the item to the ViewModel.
                    App.ViewModel.AddToDoItem(newToDoItem);
    
                    // Return to the main page.
                    if (NavigationService.CanGoBack)
                    {
                        NavigationService.GoBack();
                    }
                }
            }
    
            private void appBarCancelButton_Click(object sender, EventArgs e)
            {
                // Return to the main page.
                if (NavigationService.CanGoBack)
                {
                    NavigationService.GoBack();
                }
            }
    
    

    appBarOkButton_Click 方法对任务执行一些小验证,然后将其添加到 ViewModel。

  4. 现在您已经完成了该应用程序。按 F5 开始调试并测试该应用程序。

显示: