使用 Microsoft 桌面技术栈 – 第二部分:将 Entity Framework 4.0 与 SQL Compact 4.0 结合使用






4.58/5 (9投票s)
本系列文章解释了如何使用 Microsoft 技术栈并提供了实现它的清单。本部分将介绍如何将 Entity Framework 4.0 与 SQL Compact 4.0 结合使用。
引言
在过去几年里,Microsoft 彻底改造了其桌面应用程序技术栈,从 WinForms 转向 WPF,从 ADO.NET 转向 Entity Framework,从 Jet 数据库引擎转向 SQL Server Compact Edition。本系列文章将介绍如何使用该技术栈,并提供实现它的清单。
本系列文章共三篇
- 第 1 部分:为私有部署设置 SQL Compact 4.0
- 第 2 部分:将 Entity Framework 4.0 与 SQL Compact 4.0 结合使用
- 第 3 部分:在 MVVM 应用程序中使用 Entity Framework 4
第一部分和第二部分包含用于设置 SQL Compact 和 Entity Framework 以用于桌面应用程序的清单。第三部分演示了如何使用 MVVM 模式将 Entity Framework 4 集成到 WPF 应用程序中。演示应用程序随第三部分一起提供。
一旦项目配置为支持 SQL Compact 4.0,下一步就是为应用程序创建业务模型和数据存储。Entity Framework 4 提供了两种方法来完成这些任务:
- 数据库优先开发:如果您已经有一个数据库,可以使用 Entity Data Modeler 从数据库创建业务模型。
- 模型优先开发:如果您没有数据库,可以使用 Entity Data Modeler 在设计器表面创建模型,然后自动从该模型生成数据库。
截至 2011 年 3 月,Entity Framework 4 尚不支持处理 POCO 类,并且其对 SQL Compact 4 的桌面支持略有局限。POCO 支持已在未来的版本中承诺,并且目前处于 CTP 版本。本清单假定开发人员将使用模型优先开发,并使用 Entity Data Modeler 生成的非 POCO 对象。本清单介绍了一些在 Visual Studio 对 EF 4 与 SQL Compact 4 的支持不完整的情况下所需的变通方法。本清单假定开发人员在 Visual Studio 2010 (VS 2010) 中工作,并且正在创建一个围绕 MVVM 模式设计的 WPF 程序。
本系列第三部分中包含的演示应用程序是使用此清单设置的,它使用了本文附录中的 Repository 接口和类。演示应用程序将在第三部分中进行更详细的讨论。
请注意,Entity Framework 已内置于 .NET 4 中。应用程序的Library文件夹中不会添加任何额外的 DLL 来支持该框架。
第一步:配置应用程序
实现 EF 4 的第一步是配置将托管 Entity Data Model (EDM) 的应用程序。
步骤 1a – 设置 SQL Compact:如果尚未完成,请将应用程序设置为使用 SQL Compact 4。如果您希望为私有部署配置 SQL Compact 4(以避免版本冲突),请参阅本系列第一部分。
步骤 1b – 设置 Entity Data Model 的宿主项目:下一步是为 EF 4 Entity Data Model 设置一个宿主项目。我通常使用 Microsoft Prism 框架将应用程序划分为模块。因此,我通常将 EDM 放在一个名为 *Common* 的库项目中,该项目包含应用程序各处使用的代码和资源。我尚未发现有必要将 EDM 隔离到自己的项目中。
第二步:创建 Entity Data Model
实现 EF 4 的第二步是为应用程序创建 EDM。它包含三个要素:
- 在 VS 设计器中创建的 Entity Data Model;
- VS 从 EDM 生成的实体类;以及
- VS 也从 EDM 生成的用于持久化 EDM 的数据库。
步骤 2a – 创建 Entity Data Model:向 *Common* 项目添加 Entity Data Model (EDM)。EDM 从“添加新项”对话框中添加。
EDM 可以根据其作用命名。截图显示的是本系列第三部分附带的演示应用程序的 EDM。
EDM 将在解决方案资源管理器中显示为一个 *edmx* 文件。
步骤 2b – 向 EDM 添加实体:在 VS 2010 中打开 EDMX 文件,工作区窗格中将出现一个空白页面。打开工具箱窗格,您将看到用于创建实体和关系的工具。使用这些工具创建实体、添加属性和定义关联。
在 VS 窗口右下角的属性窗格中编辑添加到 EDM 的对象的属性。
配置实体时,请记住以下几点:
- 标量属性默认不可为空。如果属性应允许为空,请将 `Nullable` 属性设置为 `true`。
- 如果属性不可为空,请设置一个默认值。
- 要配置一个实体来存储 BLOB,请将实体数据类型设置为二进制。我们稍后将修改数据库映射以映射到 image 类型。
第三步:从 EDM 生成数据库
创建 EDM 后,下一步就是从 EDM 生成数据库。截至 2011 年 3 月,VS 2010 对此步骤的支持尚不完整。具体来说,尽管 VS 2010 的服务器资源管理器支持 SQL Compact 4.0,但“生成数据库”向导不支持它。因此,我为 SQL Compact 4.0 数据库开发了一种变通方法。一旦 VS 2010 提供对 SQL Compact 4.0 的完全支持,该变通方法便可被替换。
步骤 3a – 创建一个虚拟数据库:创建 EDM 后,右键单击工作区窗格中的空白区域,然后选择“从模型生成数据库…” (“Generate Database from Model…”)。“生成数据库”向导 (“Generate Database Wizard”) 将会启动。此向导会创建一个 SQL Compact 数据库和一个 DDL 脚本,该脚本可针对该数据库运行以将其配置为与 EDM 配合使用。
由于“生成数据库”向导不支持 SQL Compact 4.0,我们将使用该向导创建一个虚拟的 SQL Compact 3.5 数据库,然后将其丢弃。此步骤对于让向导生成我们将用于配置实际 SQL Compact 4.0 数据库的 DDL 脚本是必需的。生成的脚本实际上是 SQL Compact 3.5 脚本,但它应该可以正常运行在 SQL Compact 4.0 数据文件上。
因此,使用“生成数据库”向导创建一个虚拟 SQL Compact 3.5 数据库。名称和位置无关紧要;我通常在 Windows 桌面创建数据库,并使用默认名称。请勿立即执行 DDL 脚本,因为我们需要在将其用于配置实际数据库之前进行一些修改。
向导完成后,VS 2010 解决方案资源管理器中将出现一个新的 *EDMX.SQLCE* 文件。
我们将在后面的步骤中使用此文件来配置我们的新数据库。
步骤 3b – 修改 EDMX 文件:由于我们使用了向导来生成 SQL Compact 3.5 数据库,“生成数据库”向导会将两个对 SQL Compact 的引用嵌入到 Entity Data Model (EDMX 文件) 中。我们需要以其本机 XML 格式打开模型来更改这些引用。
为此,请在解决方案资源管理器中右键单击 EDMX 文件,选择“打开方式”(Open With),然后在出现的对话框中选择“XML (文本) 编辑器”(XML (Text) Editor)。模型文件将在 VS 2010 工作区中以 XML 文件形式打开。它看起来会像这样:
请注意 SSDL 内容部分中的 `Schema` 行中的两个对“3.5”的引用。将这些引用更改为“4.0”。
步骤 3c – 修改 BLOB 对象的映射:运行“生成数据库”向导还会将映射数据添加到 EDMX 文件的 SSDL 部分。如果您在运行向导之前在 XML 编辑器中查看了 EDMX 文件,您不会在 SSDL 内容部分找到任何映射信息。现在,“生成数据库”向导已运行,映射数据已添加到该文件部分。在进一步操作之前,我们需要修改 BLOB 对象的映射。
EDM 二进制属性通常映射到 SQL Compact 的 `varbinary` 类型。但是,`varbinary` 类型最大只能容纳 8K 字符,这使其不适合存储 BLOB 对象。因此,如果 EDM 包含任何 BLOB 属性,我们必须手动将表映射列重置为 image 类型。必须在从 EDM 生成数据库之前完成此操作。
要重置列,请在 EDMX 文件(仍应在 XML 编辑器中打开)中搜索“varbinary”。
您应该会在 EDMX 文件的 `` 部分找到一个匹配项。手动将 `varbinary` 引用替换为 `image` 引用。对每个 BLOB 属性都执行此操作。现在您已准备好创建数据库。
步骤 3d – 创建实际数据库:使用 VS 2010 服务器资源管理器创建应用程序将使用的实际 SQL Compact 4.0 数据库。通过右键单击“数据连接”(Data Connections) 节点并从出现的上下文菜单中选择“添加连接”(Add Connection) 来创建数据库。在“添加连接”对话框中,选择 SQL Compact 作为数据源,并指定应用程序将使用的数据文件的名称和位置。
步骤 3e – 修改 DDL 脚本:在执行 DDL 脚本之前,我们需要修改它以考虑 EDM 中的任何 BLOB 属性。即使我们已经更改了上面的映射,“生成数据库”向导生成的 DDL 脚本仍然为对应于 EDM 二进制属性的任何数据表列指定 `varbinary` 类型。因此,在执行脚本之前,我们需要在此脚本中将这些引用更改为 image 类型。
在 VS 2010 中打开“生成数据库”向导创建的 *EDMX.SQLCE* 文件。
使用 VS 2010 的“查找和替换”对话框很容易做到这一点。
步骤 3f – 执行 DDL 脚本:此时,我们已准备好在应用程序将使用的 SQL Compact 4.0 数据库上执行 DDL 脚本。在 VS 2010 中打开“生成数据库”向导创建的 *EDMX.SQLCE* 文件,并通过右键单击 SQL 脚本,从出现的上下文菜单中选择“连接 > 连接”(Connection > Connect) 来连接到 SQL Compact 4.0 数据库,然后按照提示操作。然后,再次右键单击并选择“执行 SQL”(Execute SQL) 来执行 *edmx.sqlce* 脚本。
DDL 脚本将执行,VS 2010 将在脚本下方显示成功消息。
如果脚本失败,或者在执行脚本时遇到可忽略的错误,则会以红色显示错误消息。
第四步:创建持久化层
实现 EF 4 的下一步是为应用程序创建持久化(数据访问)层。本文中的持久化层设计为在 Web 应用程序的情况下为每个请求使用一个新的 EF 4 `ObjectContext`,在桌面应用程序的情况下为每个 WPF 窗体使用一个新的 `ObjectContext`。持久化层使用 Repository 模式来组织数据访问。大部分工作由一个抽象的 `RepositoryBase
步骤 4a – 向应用程序添加 IRepository 接口:附录 A 包含一个 `IRepository
步骤 4a – 向应用程序添加 Repository 基类:附录 B 包含一个 `RepositoryBase
步骤 4b – 为实体创建 Repository 类:附录 C 显示了一个派生自附录 B 中 `RepositoryBase
请注意,在调用 `RepositoryBase
第五步:构建 ViewModel
假定应用程序围绕 MVVM 模式设计,下一步是为应用程序构建一个或多个 ViewModel。ViewModel 应该完全独立于对 Entity Framework 或 SQL Compact 的任何了解—持久化层的作用之一就是提供这种隔离。附录 D 包含一个 `ViewModelBase` 类,它实现了 WPF 数据绑定所需的 `INotifyPropertyChanged` 接口。它还实现了 `INotifyPropertyChanging` 接口,该接口提供属性更改的预更改通知。该类将在本系列第三部分中进行讨论。
ViewModel 的集合属性通常是 `ObservableCollection
`FsObservableCollection
第六步:添加命令和逻辑服务
应用程序将需要*命令*和*逻辑服务*来实现其用例。一般而言,命令由 ViewModel 的*命令属性*调用,而逻辑服务则由命令、ViewModel 方法和非命令属性调用。
命令包含执行特定用例所需的主要代码。为了避免命令变得臃肿,它可以将任务委托给逻辑服务类。任何被多个命令使用的代码都应移至逻辑服务类,而由单个命令使用的冗长或复杂代码也应移至逻辑服务类。任何由单个或多个 ViewModel 中的多个方法调用的复杂代码也应移至逻辑服务类。ViewModel 应保持尽可能干净整洁,它是一个协调者,而不是一个控制器。
命令和逻辑服务的实现因应用程序而异,因此我不会尝试进行分步分析。总的来说,我尽量减少命令对象和 ViewModel 中的私有方法数量,我更倾向于委托给逻辑服务类。
结论
本系列最后一篇将介绍一个端到端的演示,展示如何将 EF4 集成到围绕 MVVM 模式设计的 WPF 应用程序中。一如既往,我欢迎您提出改进本系列的意见和建议。我认为 CodeProject 读者提供的同行评审非常有价值,并且一直受到赞赏。
附录 A:IRepository 接口
以下接口指定了 Entity Framework 4 实体 Repository 类的契约。Repository 将在本系列第三部分中讨论。
using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
namespace MsDesktopStackDemo.Persistence.Interfaces
{
/// <summary>
/// An interface for an object repository.
/// </summary>
/// <typeparam name="T">The type of the entity served by a derived class.</typeparam>
/// <see>
/// http://geekswithblogs.net/seanfao/archive/2009/12/03/136680.aspx
/// </see>
public interface IRepository<T> : IDisposable where T : class
{
IQueryable<T> Fetch();
IEnumerable<T> GetAll();
IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
T Single(Expression<Func<T, bool>> predicate);
T First(Expression<Func<T, bool>> predicate);
void Add(T entity);
void Delete(T entity);
void Attach(T entity);
void SaveChanges();
void SaveChanges(SaveOptions options);
}
}
附录 B:RepositoryBase 类
以下类可用作 Entity Framework 4 Repository 的基类。它将在本系列第三部分中讨论。该 Repository 允许使用拥有或共享的对象上下文。拥有对象上下文将在 Repository 被释放时被释放。
请注意,第一个构造函数(拥有对象上下文)接受三个参数:文件路径、要构建的对象上下文类型以及该类所服务的 Entity Data Model 的名称。文件路径由调用代码提供,其余参数由派生自此类的具体 Repository 在其构造函数中的 `base()` 调用中提供。请参阅附录 C 中的 `BookRepository` 示例具体类。
using System;
using System.Collections.Generic;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
using NoteMaster3.Common.Interfaces;
namespace NoteMaster3.Common.BaseClasses
{
/// <summary>
/// A base class for an object set repository.
/// </summary>
/// <typeparam name="T">The type served by concrete implementations of
/// this class.</typeparam>
/// <remarks>This repository manages an implementation Microsoft Entity
/// Framework 4.</remarks>
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
#region Fields
// Private member variables
private ObjectContext m_ObjectContext;
private IObjectSet<T> m_ObjectSet;
private bool m_UsingSharedObjectContext;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the Repository class with its own object context.
/// </summary>
/// <param name="filePath">The path to the data file.</param>
/// <param name="contextType">The type of the EF4 object contex tcreated by
/// this repository.</param>
/// <param name="edmName">The name of the Entity Data Model served by this
/// repository.</param>
/// <remarks>
/// The object context for an EDM is typed to the EDM. The type can be found
/// in the Designer code for the EDM; it is the class that derives from
/// ObjectContext. The type is passed to this constructor by a base() call
/// from the constructor of a derived class.
/// </remarks>
protected RepositoryBase(string filePath, Type contextType, string edmName)
{
m_ObjectContext = this.CreateObjectContext(filePath, contextType, edmName);
m_ObjectSet = m_ObjectContext.CreateObjectSet<T>();
m_UsingSharedObjectContext = false;
}
/// <summary>
/// Initializes a new instance of the Repository class with a shared object context.
/// </summary>
/// <param name="objectContext">An Entity Framework 4 object context.</param>
protected RepositoryBase(ObjectContext objectContext)
{
m_ObjectContext = objectContext;
m_ObjectSet = m_ObjectContext.CreateObjectSet<T>();
m_UsingSharedObjectContext = true;
}
#endregion
#region Public Methods
/// <summary>
/// Adds the specified entity
/// </summary>
/// <param name="entity">Entity to add</param>
/// <exception cref="ArgumentNullException"> if <paramref name="entity"/> is
/// null</exception>
public void Add(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
m_ObjectSet.AddObject(entity);
}
/// <summary>
/// Attaches the specified entity
/// </summary>
/// <param name="entity">Entity to attach</param>
public void Attach(T entity)
{
m_ObjectSet.Attach(entity);
}
/// <summary>
/// Gets all records as an IQueryable
/// </summary>
/// <returns>An IQueryable object containing the results of the query</returns>
public IQueryable<T> Fetch()
{
return m_ObjectSet;
}
/// <summary>
/// Deletes the specified entitiy
/// </summary>
/// <param name="entity">Entity to delete</param>
/// <exception cref="ArgumentNullException"> if <paramref name="entity"/> is
/// null</exception>
public void Delete(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
m_ObjectSet.DeleteObject(entity);
}
/// <summary>
/// Deletes records matching the specified criteria
/// </summary>
/// <param name="predicate">Criteria to match on</param>
public void Delete(Expression<Func<T, bool>> predicate)
{
var records = from x in m_ObjectSet.Where(predicate) select x;
foreach (T record in records)
{
m_ObjectSet.DeleteObject(record);
}
}
/// <summary>
/// Releases all resources used by the Repository.
/// </summary>
public void Dispose()
{
/* See http://msdn.microsoft.com/en-us/library/system.idisposable.dispose.aspx */
// Call the protected method in this class
var disposeOfObjectContext = (m_UsingSharedObjectContext == false);
Dispose(disposeOfObjectContext);
// Dispose() will finalize this object, so take it out of the queue
GC.SuppressFinalize(this);
}
/// <summary>
/// Finds a record with the specified criteria
/// </summary>
/// <param name="predicate">Criteria to match on</param>
/// <returns>A collection containing the results of the query</returns>
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
{
return m_ObjectSet.Where(predicate);
}
/// <summary>
/// The first record matching the specified criteria
/// </summary>
/// <param name="predicate">Criteria to match on</param>
/// <returns>A single record containing the first record matching the specified
/// criteria</returns>
public T First(Expression<Func<T, bool>> predicate)
{
return m_ObjectSet.First(predicate);
}
/// <summary>
/// Gets all records as an IEnumberable
/// </summary>
/// <returns>An IEnumberable object containing the results of the query</returns>
public IEnumerable<T> GetAll()
{
return Fetch().AsEnumerable();
}
/// <summary>
/// Saves all context changes
/// </summary>
public void SaveChanges()
{
m_ObjectContext.SaveChanges();
}
/// <summary>
/// Saves all context changes with the specified SaveOptions
/// </summary>
/// <param name="options">Options for saving the context</param>
public void SaveChanges(SaveOptions options)
{
m_ObjectContext.SaveChanges(options);
}
/// <summary>
/// Gets a single record by the specified criteria (usually the unique identifier)
/// </summary>
/// <param name="predicate">Criteria to match on</param>
/// <returns>A single record that matches the specified criteria</returns>
public T Single(Expression<Func<T, bool>> predicate)
{
return m_ObjectSet.Single(predicate);
}
#endregion
#region Protected Methods
/// <summary>
/// Releases all resources used by the Repository.
/// </summary>
/// <param name="disposing">A boolean value indicating
/// whether or not to dispose managed
/// resources</param>
protected virtual void Dispose(bool disposing)
{
/* See http://msdn.microsoft.com/en-us/library/system.idisposable.dispose.aspx */
if (!disposing) return;
if (m_ObjectContext == null) return;
m_ObjectContext.Dispose();
m_ObjectContext = null;
}
#endregion
#region Private Methods
/// <summary>
/// Factory method to create an Entity Framework 4 ObjectContext.
/// </summary>
/// <param name="filePath">The path to the target data file.</param>
/// <param name="contextType">The type of the EF4 object context created by this
/// repository.</param>
/// <param name="edmName">The name of the Entity Data Model served by this
/// repository.</param>
/// <returns>A new ObjectContext.</returns>
private ObjectContext CreateObjectContext(string filePath, Type contextType,
string edmName)
{
// Validate EDM Name
if (edmName == null)
{
throw new ArgumentException("Argument 'edmName' passed in was null.");
}
// Check file path
if (filePath == null)
{
throw new ArgumentException("Argument 'filePath' passed in was null.");
}
// Configure a SQL CE connection string
var sqlCompactConnectionString = string.Format("Data Source={0}", filePath);
// Create an Entity Connection String Builder
var builder = new EntityConnectionStringBuilder();
/* The builder creates an EDM connection string. It expects to receive the
* EDM model name; e.g., "Model.Books", as opposed to "BooksContainer", which
* will be the name of the ObjectContext generated by this method. */
/* The easiest way to verify the EDM model name is to generate a database from
* the EDM. The Create Database Wizard has an option to write an EDM connection
* string to the App.config file. Accept the option, and compare the resulting
* connection string to the metadata string below. */
/* Note that the value of the m_EdmName variable is set in the constructor of
* a derived class. */
// Configure Builder
builder.Metadata = string.Format(
"res://*/{0}.csdl|res://*/{0}.ssdl|res://*/{0}.msl", edmName);
builder.Provider = "System.Data.SqlServerCe.4.0";
builder.ProviderConnectionString = sqlCompactConnectionString;
var edmConnectionString = builder.ToString();
// Create an EDM connection
var edmConnection = new EntityConnection(edmConnectionString);
// Get the object context
var context = Activator.CreateInstance(contextType, edmConnection);
// Set return value
return (ObjectContext)context;
}
#endregion
}
}
附录 C:示例具体 Repository
以下类是附录 B 中所示基类的具体实现的示例。它将在本系列第三部分中进行讨论。
该类依赖于基类来完成所有工作。请注意,构造函数接受一个参数,即要打开的数据文件的文件路径。它通过 `base()` 调用将此参数传递给基类,同时传递要创建的数据上下文类型以及 Repository 所服务的 Entity Data Model 的名称。请注意,后两个参数已硬编码在具体 Repository 声明中,并且调用代码无需了解 EF4。
using MsDesktopStackDemo.Persistence.BaseClasses;
using MsDesktopStackDemo.Model;
namespace MsDesktopStackDemo.Persistence
{
/// <summary>
/// A repository for Book entities.
/// </summary>
public class BookRepository : RepositoryBase<Book>
{
#region Fields
// Member variables
private static Type m_ContextType = typeof(BooksContainer);
private static string m_EdmName = "Model.Books";
#endregion
#region Constructor
/// <summary>
/// Default constructor
/// </summary>
/// <param name="filePath">A path to the target data fie.</param>
/// <remarks>
/// Note that the constructor hard-codes the name of the Entity Data Model served
/// by this Repository. We hard code the value, rather than pass it in, to isolate
/// the caller from any knowledge of Entity Framework 4. The result is looser
/// coupling between Entity Framework 4 and the rest of the application.
/// </remarks>
public BookRepository(string filePath) : base(filePath, m_ContextType, m_EdmName)
{
}
#endregion
}
}
附录 D:ViewModelBase 类
本附录包含一个 MVVM ViewModel 的基类。它实现了 WPF 数据绑定所需的 `INotifyPropertyChanged` 接口,并实现了 `INotifyPropertyChanging` 接口,该接口提供属性更改的预更改通知。该类将在本系列第三部分中进行讨论。
using System.ComponentModel;
namespace MsDesktopStackDemo.ViewModel.BaseClasses
{
public abstract class ViewModelBase :
INotifyPropertyChanging, INotifyPropertyChanged
{
#region INotifyPropertyChanging Members
public event PropertyChangingEventHandler PropertyChanging;
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Administrative Properties
/// <summary>
/// Whether the view model should ignore property-change events.
/// </summary>
public virtual bool IgnorePropertyChangeEvents { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">The name of the changed property.</param>
public virtual void RaisePropertyChangedEvent(string propertyName)
{
// Exit if changes ignored
if (IgnorePropertyChangeEvents) return;
// Exit if no subscribers
if (PropertyChanged == null) return;
// Raise event
var e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
/// <summary>
/// Raises the PropertyChanging event.
/// </summary>
/// <param name="propertyName">The name of the changing property.</param>
public virtual void RaisePropertyChangingEvent(string propertyName)
{
// Exit if changes ignored
if (IgnorePropertyChangeEvents) return;
// Exit if no subscribers
if (PropertyChanging == null) return;
// Raise event
var e = new PropertyChangingEventArgs(propertyName);
PropertyChanging(this, e);
}
#endregion
}
}
附录 E:可感知 Repository 的可观察集合
本附录包含一个派生自 .NET `ObservableCollection
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using MsDesktopStackDemo.Persistence.Interfaces;
using MsDesktopStackDemo.ViewModel.Events;
namespace MsDesktopStackDemo.ViewModel.BaseClasses
{
/// <summary>
/// An ObservableCollection for repository-aware collections.
/// </summary>
/// <typeparam name="T">The type of EF4 entity served.</typeparam>
public class FsObservableCollection<T> : ObservableCollection<T> where T : class
{
#region Fields
// Member variables
private readonly IRepository<T> m_Repository;
#endregion
#region Constructors
/// <summary>
/// Creates a new FS Observable Collection and populates it with a list of items.
/// </summary>
/// <param name="items">The items to be inserted into the collection.</param>
/// <param name="repository">The Repository for type T.</param>
public FsObservableCollection(IEnumerable<T> items, IRepository<T> repository)
: base(items ?? new T[] { })
{
/* The base class constructor call above uses the null-coalescing operator (the
* double-question mark) which specifies a default value if the value passed in
* is null. The base class constructor call passes a new empty array of type t,
* which has the same effect as calling the constructor with no parameters--
* a new, empty collection is created. */
if (repository == null) throw new ArgumentNullException("repository");
m_Repository = repository;
}
/// <summary>
/// Creates an empty FS Observable Collection, with a repository.
/// </summary>
/// <param name="repository">The Repository for type T.</param>
public FsObservableCollection(IRepository<T> repository) : base()
{
m_Repository = repository;
}
#endregion
#region Events
/// <summary>
/// Occurs before the collection changes,
/// providing the opportunity to cancel the change.
/// </summary>
public event CollectionChangingEventHandler<T> CollectionChanging;
#endregion
#region Protected Method Overrides
/// <summary>
/// Inserts an element into the Collection at the specified index.
/// </summary>
/// <param name="index">The zero-based index
// at which item should be inserted.</param>
/// <param name="item">The object to insert.</param>
protected override void InsertItem(int index, T item)
{
// Raise CollectionChanging event; exit if change cancelled
var newItems = new List<T>(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
// Insert new item
base.InsertItem(index, item);
m_Repository.Add(item);
}
/// <summary>
/// Removes the item at the specified index of the collection.
/// </summary>
/// <param name="index">The zero-based index of the element to remove.</param>
protected override void RemoveItem(int index)
{
// Initialize
var itemToRemove = this[index];
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List<T>(new[] { itemToRemove });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
// Remove new item
base.RemoveItem(index);
m_Repository.Delete(itemToRemove);
}
/// <summary>
/// Removes all items from the collection.
/// </summary>
protected override void ClearItems()
{
// Initialize
var itemsToDelete = this.ToArray();
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List<T>(itemsToDelete);
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
// Removes all items from the collection.
base.ClearItems();
foreach (var item in itemsToDelete)
{
m_Repository.Delete(item);
}
}
/// <summary>
/// Replaces the element at the specified index.
/// </summary>
/// <param name="index">The zero-based index of the element to replace.</param>
/// <param name="newItem">The new value for
// the element at the specified index.</param>
protected override void SetItem(int index, T newItem)
{
// Initialize
var itemToReplace = this[index];
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List<T>(new[] { itemToReplace });
var newItems = new List<T>(new[] { newItem });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Replace, oldItems, newItems);
if (cancelled) return;
// Rereplace item
base.SetItem(index, newItem);
m_Repository.Delete(itemToReplace);
m_Repository.Add(newItem);
}
#endregion
#region Public Method Overrides
/// <summary>
/// Adds an object to the end of the collection.
/// </summary>
/// <param name="item">The object to be
/// added to the end of the collection.</param>
public new void Add(T item)
{
// Raise CollectionChanging event; exit if change cancelled
var newItems = new List<T>(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
// Add new item
base.Add(item);
m_Repository.Add(item);
}
/// <summary>
/// Removes all elements from the collection and from the data store.
/// </summary>
public new void Clear()
{
/* We call the overload of this method with the 'clearFromDataStore'
* parameter, hard-coding its value as true. */
// Call overload with parameter
this.Clear(true);
}
/// <summary>
/// Removes all elements from the collection.
/// </summary>
/// <param name="clearFromDataStore">Whether the items
/// should also be deleted from the data store.</param>
public void Clear(bool clearFromDataStore)
{
// Initialize
var itemsToDelete = this.ToArray();
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List<T>(itemsToDelete);
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
// Remove all items from the collection.
base.Clear();
// Exit if not removing from data store
if (!clearFromDataStore) return;
// Remove all items from the data store
foreach (var item in itemsToDelete)
{
m_Repository.Delete(item);
}
}
/// <summary>
/// Inserts an element into the collection at the specified index.
/// </summary>
/// <param name="index">The zero-based index
// at which item should be inserted.</param>
/// <param name="item">The object to insert.</param>
public new void Insert(int index, T item)
{
// Raise CollectionChanging event; exit if change cancelled
var newItems = new List<T>(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
// Insert new item
base.Insert(index, item);
m_Repository.Add(item);
}
/// <summary>
/// Persists changes to the collection to the data store.
/// </summary>
public void PersistToDataStore()
{
m_Repository.SaveChanges();
}
/// <summary>
/// Removes the first occurrence of a specific object from the collection.
/// </summary>
/// <param name="itemToRemove">The object
/// to remove from the collection.</param>
public new void Remove(T itemToRemove)
{
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List<T>(new[] { itemToRemove });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
// Remove target item
base.Remove(itemToRemove);
m_Repository.Delete(itemToRemove);
}
#endregion
#region Private Methods
/// <summary>
/// Raises the CollectionChanging event.
/// </summary>
/// <returns>True if a subscriber cancelled
/// the change, false otherwise.</returns>
private bool RaiseCollectionChangingEvent(
NotifyCollectionChangingAction action, IList<T> oldItems,
IList<T> newItems)
{
// Exit if no subscribers
if (CollectionChanging == null) return false;
// Create event args
var e = new NotifyCollectionChangingEventArgs<T>(action, oldItems, newItems);
// Raise event
this.CollectionChanging(this, e);
/* Subscribers can set the Cancel property on the event args; the
* event args will reflect that change after the event is raised. */
// Set return value
return e.Cancel;
}
#endregion
}
}