Code First 演练:更新模型并绑定到它
快速地向模型添加实体,将它们相互关联,并在 XAML 中绑定到它们
引言
DevForce AOP 实体不仅仅是存储在数据库中的数据集合。它们可以参与到客户端 UI 中。它们拥有用于缓存、导航到相关实体、数据绑定、验证和更改跟踪的隐藏基础设施。
上次,我们确定了一个实体类可以完全用代码构建。该类型的实例可以被创建和保存。但我们没有展示相关实体,并且我们仅仅是将实体属性值转储为显示在屏幕上的字符串。
这一次,我们将展示您可以快速地向模型添加实体,将它们相互关联,并在 XAML 中绑定到它们。
- 平台: WPF
- 语言: C#
- 下载: Code First 演练
背景
应用程序本身不是重点。我们并不追求创建一个有用的应用程序,也不展示 UI 设计或开发的最佳实践。我们的目的是引导您完成一系列步骤,介绍在 DevForce Code First 风格中编写和使用实体模型的要点。
本教程展示了如何使用 Code First 构建一个简单的 WPF 应用程序。您将定义一个单一实体(Category
)模型,将其实例添加到开发数据库中,将它们查询回来,将它们格式化为字符串,并在屏幕上显示为日志消息。
DevForce 实现了一个端到端的 n 层架构。它通过在服务器上公开一个通用的 Web 服务来工作,该服务接收 LINQ 表达式,并通过 Internet 将业务对象返回到客户端。
DevForce 为您处理所有 n 层 WCF 通信、序列化和封送。
剧透警告:所有 CF 实体都是 DevForce 实体,并拥有与从 EDMX 生成的 DevForce 实体相同的优势。
开始之前
必须安装 DevForce 和 Entity Framework 4.1。
本次演练使用 SQL Server Express,它直接与 Entity Framework 的 Code First 功能集成。如果您想使用非 Express 版本的 SQL Server,请参阅此部分:摆脱 SQL Server Express 的生活。
向模型添加 Product 实体
- 打开 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
指向其子产品添加一个导航属性……但我们不必这样做,在本例中我们也不会这样做。 - 向
ProductEntities
(我们的自定义EntityManager
)添加一个方便的Product
查询属性。public EntityQuery<Product> Products { get; set; }
- 再次构建以保存模型并重新生成元数据。
提示:每次更改模型后都要构建。查看 Output 窗口,并确认元数据 .ibmmx 文件已重新生成。
更新视图
- 打开视图 MainWindow.xaml。确保 XAML 窗格可见。
- 通过添加
RowDefinition
为网格添加一行。现在应该有三行。<Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions>
- 添加 一个
GridSplitter
来分隔第二行和第三行,并允许用户显示更多或更少的第二行内容。<GridSplitter Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="8"/>
- 在第二行添加一个
ListBox
。 - 将
ListBox.ItemSource
绑定到ViewModel
中的Products
集合属性;我们还没有编写该属性,但很快就会写。 - 将
ListBox.SelectedItem
绑定到另一个未来的ViewModel
属性,即SelectedProduct
,以便用户的选择能够传达给ViewModel
。 - 将
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}"/>
- 滚动到 XAML 接近顶部的位置,就在
<Grid>
标签上方。 - 在
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
实体并显示它们。
- 打开
MainWindowViewModel
类,然后转到AddTestData()
方法。 - 将类别创建重构为一个
AddNewCategory()
方法和一个CurrentCategory
属性。private void AddNewCategory() { CurrentCategory = new Category { CategoryName = "Sweet Things on " + DateTime.Now.ToString("o") }; Manager.AddEntity(CurrentCategory); } private Category CurrentCategory { get; set; }
- 编写一个
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
;重复添加两次也没有坏处。
- 修改
AddTestData
方法以使用这些新的 ViewModel 成员。private void AddTestData() { AddNewCategory(); AddNewProduct("Chocolate Cream Pie"); AddNewProduct("Baklava"); Log("Saving new test data"); Manager.SaveChanges(); }
- 转到
ShowTestData()
,我们将在此显示新产品。 - 在
ShowTestData();
下方添加一个Products
属性;这是要显示的产品数据绑定集合。public ObservableCollection<Product> Products { get; set; }
- 查询
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>
。作为迭代它的副产品,它无意中执行了一次查询。 - “仅缓存”
我们想证明我们确实在检索
Product
时带入了Category
。我们不希望Product
的Category
属性(例如someProduct.Category
)执行延迟加载。一种防止延迟加载的方法是告诉
Manager
,它默认不再查询数据库。我们将Manager
的DefaultQueryStrategy
改为“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 }
- 删除所有
Categories
的日志记录,包括此方法中的那一行和LogCats
方法。 - 查看 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
值的显示。
您不记得编写了 EntityAspect
或 EntityState
属性。您也不记得实现了更改通知。那是因为您没有。查看 Product
的源代码;它们不在那里。
该实体在因 DevForce AOP 而重写时获得了这些属性和功能。
摘要
在 Code First 演练的这一部分中,我们
- 向模型添加了新实体……而无需使用设计器或修改数据库。
- 使用急切加载(“
Include
”)检索了带有其Products
的Categories
。 - 将 UI 控件绑定到实体,利用了 DevForce AOP 实体数据绑定支持。
- 通过绑定到注入到 DevForce AOP 实体的
EntityAspect
属性,显示了实体的状态。
历史
- v.1: 原始