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

Code First 演练:更新模型并绑定到它

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2011年11月18日

CPOL

7分钟阅读

viewsIcon

20192

downloadIcon

269

快速地向模型添加实体,将它们相互关联,并在 XAML 中绑定到它们

引言

DevForce AOP 实体不仅仅是存储在数据库中的数据集合。它们可以参与到客户端 UI 中。它们拥有用于缓存、导航到相关实体、数据绑定、验证和更改跟踪的隐藏基础设施。

上次,我们确定了一个实体类可以完全用代码构建。该类型的实例可以被创建和保存。但我们没有展示相关实体,并且我们仅仅是将实体属性值转储为显示在屏幕上的字符串。

这一次,我们将展示您可以快速地向模型添加实体,将它们相互关联,并在 XAML 中绑定到它们。

背景

应用程序本身不是重点。我们并不追求创建一个有用的应用程序,也不展示 UI 设计或开发的最佳实践。我们的目的是引导您完成一系列步骤,介绍在 DevForce Code First 风格中编写和使用实体模型的要点。

本教程展示了如何使用 Code First 构建一个简单的 WPF 应用程序。您将定义一个单一实体(Category)模型,将其实例添加到开发数据库中,将它们查询回来,将它们格式化为字符串,并在屏幕上显示为日志消息。

DevForce 实现了一个端到端的 n 层架构。它通过在服务器上公开一个通用的 Web 服务来工作,该服务接收 LINQ 表达式,并通过 Internet 将业务对象返回到客户端。

DevForce 为您处理所有 n 层 WCF 通信、序列化和封送。

剧透警告:所有 CF 实体都是 DevForce 实体,并拥有与从 EDMX 生成的 DevForce 实体相同的优势。

开始之前

必须安装 DevForceEntity Framework 4.1

本次演练使用 SQL Server Express,它直接与 Entity Framework 的 Code First 功能集成。如果您想使用非 Express 版本的 SQL Server,请参阅此部分:摆脱 SQL Server Express 的生活

向模型添加 Product 实体

  1. 打开 Model 类,并添加以下 Product 类定义
    [ProvideEntityAspect]
    public class Product
    {
       public int ProductId { get; set; }
       public string ProductName { get; set; }
    
       public int CategoryId { get; set; } // Foreign key for "Category"
       public Category Category { get; set; }
    }

    请注意,Product 有一个指向其父 Category 的导航属性。我们可以从 Category 指向其子产品添加一个导航属性……但我们不必这样做,在本例中我们也不会这样做。

  2. ProductEntities(我们的自定义 EntityManager)添加一个方便的 Product 查询属性。
    public EntityQuery<Product> Products { get; set; }
  3. 再次构建以保存模型并重新生成元数据。

提示:每次更改模型后都要构建。查看 Output 窗口,并确认元数据 .ibmmx 文件已重新生成。

更新视图

  1. 打开视图 MainWindow.xaml。确保 XAML 窗格可见。
  2. 通过添加 RowDefinition 为网格添加一行。现在应该有三行。
    <Grid.RowDefinitions>
        <RowDefinition Height="40" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
  3. 添加 一个 GridSplitter 来分隔第二行和第三行,并允许用户显示更多或更少的第二行内容。
    <GridSplitter Grid.Row="2" HorizontalAlignment="Stretch"  
        VerticalAlignment="Top" Height="8"/>
  4. 第二行添加一个 ListBox
  5. ListBox.ItemSource 绑定到 ViewModel 中的 Products 集合属性;我们还没有编写该属性,但很快就会写。
  6. ListBox.SelectedItem 绑定到另一个未来的 ViewModel 属性,即 SelectedProduct,以便用户的选择能够传达给 ViewModel
  7. ListBox.ItemTemplate 绑定到即将添加的 ProductTemplate,以便为 Products 集合中的每个项目向用户呈现一个格式良好的产品。

    这是完成后的 ListBox XAML

    <ListBox x:Name="productsListBox" Grid.Row="2" Margin="0,10,0,0"
              SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"
              ItemsSource="{Binding Products}"
              ItemTemplate="{StaticResource ProductTemplate}"/>
  8. 滚动到 XAML 接近顶部的位置,就在 <Grid> 标签上方。
  9. Window.Resources 标签中添加以下 Product DataTemplate
    <Window.Resources>
        <DataTemplate x:Key="ProductTemplate">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" MinHeight="30"/>
                    <RowDefinition Height="Auto" MinHeight="30"/>
                    <RowDefinition Height="20" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
    
                <TextBlock Text="Product: " Grid.Row="0" Grid.Column="0"
                           VerticalAlignment="Center" />
                <TextBlock Text="{Binding ProductId}" Grid.Row="0" Grid.Column="1"
                           VerticalAlignment="Center" Margin="2,0,2,0" MinWidth="20"/>
                <TextBox Text="{Binding ProductName, Mode=TwoWay, 
            ValidatesOnDataErrors=True}"
                         Grid.Row="0" Grid.Column="2"
                         VerticalAlignment="Center" Margin="2,0,2,0" MinWidth="100"/>
                <TextBlock Text="NotPersisted: " Grid.Row="0" Grid.Column="3"
                           VerticalAlignment="Center" Margin="8,2,0,0"/>
                <TextBox Text="{Binding NotPersisted, Mode=TwoWay, 
            ValidatesOnDataErrors=True}"
                         Grid.Row="0" Grid.Column="4"
                         VerticalAlignment="Center" MinWidth="100"/>
                <TextBlock Text="{Binding EntityAspect.EntityState}" Grid.Row="0" 
            Grid.Column="5"
                           VerticalAlignment="Center" Margin="8,0,0,0"/>
    
                <TextBlock Text="Category: " Grid.Row="1" Grid.Column="0"
                           VerticalAlignment="Center" Margin="8,0,0,0"/>
                <TextBlock Text="{Binding CategoryId}" Grid.Row="1" Grid.Column="1"
                           VerticalAlignment="Center" Margin="2,0,2,0" MinWidth="20"/>
                <TextBox Text="{Binding Category.CategoryName, Mode=TwoWay, 
            ValidatesOnDataErrors=True}"
                         Grid.Row="1" Grid.Column="2"
                         VerticalAlignment="Center" Grid.ColumnSpan="4" />
    
                <TextBlock Text="Supplier: " Grid.Row="2" Grid.Column="0"
                           VerticalAlignment="Center" Margin="8,0,0,0" />
                <TextBox Text="{Binding Supplier.CompanyName, Mode=TwoWay, 
            ValidatesOnDataErrors=True}"
                         Grid.Row="2" Grid.Column="2"
                         VerticalAlignment="Center" Grid.ColumnSpan="4"/>
            </Grid>
        </DataTemplate>
    </Window.Resources>

哇……这 XAML 可真多!

我们不需要马上全部用上。如果您仔细阅读,您会看到绑定到我们尚未定义的属性和类型的语句。

如果您了解 XAML 绑定,其中没有任何令人惊讶的地方,也没有声称您除了在示例中粘贴这些内容之外别无选择,并且也没有必要稍后粘贴更多的模板 XAML。

修改 UI 以显示产品

接下来,我们创建一些新的 Product 实体并显示它们。

  1. 打开 MainWindowViewModel 类,然后转到 AddTestData() 方法。
  2. 类别创建重构为一个 AddNewCategory() 方法和一个 CurrentCategory 属性。
    private void AddNewCategory()
    {
        CurrentCategory =
           new Category { CategoryName = "Sweet Things on " + 
                    DateTime.Now.ToString("o") };
        Manager.AddEntity(CurrentCategory);
    }
    private Category CurrentCategory { get; set; }
  3. 编写一个 AddNewProduct() 方法。
    private Product AddNewProduct(string productName = "A new product")
    {
        var newProduct = new Product
        {
            ProductName = productName,
            Category = CurrentCategory,
        };
    
       //Manager.AddEntity(newProduct); // harmless but unnecessary
       return newProduct;

    一些值得注意的点

    • 调用者可以提供新的产品名称,否则该方法将提供一个默认名称。
    • 尽管该方法没有显式地将新的 Product 实例添加到 Manager,但将其 Category 设置为已在 Manager 中的实体(CurrentCategory)会使新 Product 绘制进来。
    • 如果您愿意,也可以将其显式添加到 Manager;重复添加两次也没有坏处。
  4. 修改 AddTestData 方法以使用这些新的 ViewModel 成员。
    private void AddTestData()
    {
        AddNewCategory();
        AddNewProduct("Chocolate Cream Pie");
        AddNewProduct("Baklava");
        Log("Saving new test data");
        Manager.SaveChanges();
    }
  5. 转到 ShowTestData(),我们将在此显示新产品。
  6. ShowTestData(); 下方添加一个 Products 属性;这是要显示的产品数据绑定集合。
    public ObservableCollection<Product> Products { get; set; }
  7. 查询 Products
    这次,我们将不查询 Categories,而是查询 Products。我们还告诉 DevForce “Include”……也就是说,在获取 Products 时,将父 Category 一起带上。
    Include 是一个 DevForce 扩展方法;我们需要 using IdeaBlade.EntityModel;
    var productQuery = Manager.Products.Include("Category");

    我们已经定义了查询,但尚未运行它。尚未检索到任何实体。
    用查询结果初始化 Products 集合。

    Products = new ObservableCollection<Product>
            (productQuery); // executes & adds to list

    ObservableCollection<T> 的构造函数将查询视为一个 IEnumerable<T>。作为迭代它的副产品,它无意中执行了一次查询。

  8. “仅缓存”

    我们想证明我们确实在检索 Product 时带入了 Category。我们不希望 ProductCategory 属性(例如 someProduct.Category)执行延迟加载。

    一种防止延迟加载的方法是告诉 Manager,它默认不再查询数据库。我们将 ManagerDefaultQueryStrategy 改为“CacheOnly”。

    // DEMO ONLY – DO NOT DO THIS IN YOUR CODE
    Manager.DefaultQueryStrategy = 
        QueryStrategy.CacheOnly; // let's see only what's in cache

    注意:我们仍然可以显式地查询数据库,但不再默认这样做。

    ShowTestData 方法的最终状态是

    private void ShowTestData()
    {
        Log("Showing test data");
        var productQuery = Manager.Products.Include("Category");
        Products = new ObservableCollection<Product>
            (productQuery); // executes & adds to list
       
       // DEMO ONLY - DO NOT DO THIS IN YOUR CODE
       Manager.DefaultQueryStrategy = 
        QueryStrategy.CacheOnly; // let's see only what's in cache
    }
  9. 删除所有 Categories 的日志记录,包括此方法中的那一行和 LogCats 方法。
  10. 查看 Window.xaml 中的 ProductTemplate(可选)。

您会在产品的 DataTemplate 中找到一行绑定到 CategoryName

<TextBox Text="{Binding Category.CategoryName, 
    Mode=TwoWay, ValidatesOnDataErrors=True}" ... />

请注意,它如何从 Product(隐式 DataContext)导航到 Category,再从那里导航到 CategoryName。这样做是因为(a)DevForce 将缓存导航注入到 Product.Category 属性中,并且(b)由于“Include”查询,相关的 Category 实例在绑定被执行时已经在缓存中。

F5 – 构建并运行(失败)

窗口报告保存和查询过程中出现故障。幸好我们在记录故障!

初始保存期间的错误消息确切地告诉了我们问题所在:我们的模型有一个 Product 类型,但数据库没有相应的表。我们正在尝试保存 Products,但我们的数据库不知道 products

由于我们告诉 DevForce 我们已处理异常,应用程序会继续运行,并再次尝试查询产品时失败。外部异常消息没有用,但如果我们停止应用程序(Shift-F5),在 EntityServerError 处理程序上设置断点,再次运行,并查看内部异常的内容

问题是一样的。我们正在尝试查询 Products,但数据库对此一无所知。

数据库反正就是一堆垃圾,所以我们可以删除“ProductEntities”数据库,使用 SQL Server Management Studio。

在本次教程的下一部分中,我们将克服这种删除过时数据库的烦人必要性。

再次运行(F5)。这次 Entity Framework 创建了数据库,应用程序就可以正常工作了。

文本值“Unchanged”来自每个 DevForce 实体的 EntityState 属性,通过其 EntityAspect 属性,正如我们在 ProductTemplate 中的相关数据绑定行中所见。

<TextBlock Text="{Binding EntityAspect.EntityState}" Grid.Row="0" Grid.Column="5"

这意味着 Product 自我们保存以来没有改变。当我们将其从“Chocolate Cream Pie”更改为“Chocolate Rabbit”并离开输入框时,显示的 EntityState 值变为“Modified”。

Product 实体内部的 DevForce 基础设施显然在发挥作用。

  • 更改跟踪检测到了名称更改,并修改了产品的 EntityState
  • 更改通知导致表单刷新了 EntityState 值的显示。

您不记得编写了 EntityAspectEntityState 属性。您也不记得实现了更改通知。那是因为您没有。查看 Product 的源代码;它们不在那里。

该实体在因 DevForce AOP 而重写时获得了这些属性和功能。

摘要

在 Code First 演练的这一部分中,我们

  • 向模型添加了新实体……而无需使用设计器或修改数据库。
  • 使用急切加载(“Include”)检索了带有其 ProductsCategories
  • 将 UI 控件绑定到实体,利用了 DevForce AOP 实体数据绑定支持。
  • 通过绑定到注入到 DevForce AOP 实体的 EntityAspect 属性,显示了实体的状态。

历史

  • v.1: 原始
© . All rights reserved.