Introduction
Microsoft has revamped its desktop application stack in the past few years, moving from WinForms to WPF, from ADO.NET to the Entity Framework, and from the Jet database engine to SQL Server Compact Edition. This series of articles explain how to use the stack and presents checklists for implementing it.
The series consists of three articles:
Parts 1 and 2 contain checklists for setting up SQL Compact and Entity Framework for a desktop application. Part 3 shows how to integrate Entity Framework 4 into a WPF application using the MVVM pattern. The demo app is included with Part 3.
Once a project has been configured to support SQL Compact 4.0, the next step is to create a business model and a data store for the application. Entity Framework 4 provides two methods for accomplishing these tasks:
- Database-First development: If you already have a database, you can use the Entity Data Modeler to create a business model from the database.
- Model-First development: If you don’t have a database, you can create a model on a designer surface using the Entity Data Modeler, then automatically generate a database from that model.
As of March 2011, Entity Framework 4 does not provide support for working with POCO classes, and its desktop support for SQL Compact 4 is somewhat limited. POCO support has been promised for a future release, and it is currently in CTP release. This checklist assumes that the developer will be using Model-First development, using non-POCO objects generated by the Entity Data Modeler. The checklist presents several workarounds needed in areas where Visual Studio provides incomplete support for using EF 4 with SQL Compact 4. The checklist assumes the developer is working in Visual Studio 2010 (VS 2010), and that the developer is creating a WPF program designed around the MVVM pattern.
The demo app included with Part 3 of this series was set up using this checklist, and it uses the Repository interfaces and classes included in the Appendices to this article. The demo app is discussed in more detail in Part 3.
Note that the Entity Framework is built into .NET 4. No additional DLLs are added to the application’s Library folder to support the framework.
Step 1: Configure the Application
The first step in implementing EF 4 is to configure the application that will host an Entity Data Model (EDM).
Step 1a – Set up SQL Compact: Set the application up to use SQL Compact 4, if you have not already done so. If you wish to configure SQL Compact 4 for a private deployment, which avoids versioning issues, see Part 1 of this series.
Step 1b – Set the host project for the Entity Data Model: The next step is to set a host project for the EF 4 Entity Data Model. I generally partition my application into modules, using the Microsoft Prism framework. Accordingly, I typically place the EDM in a library project named Common, which houses code and resources used across the application. I haven’t found it necessary to isolate the EDM in its own project.
Step 2: Create the Entity Data Model
The second step in implementing EF 4 is to create an EDM for the application. It has three elements:
- The Entity Data Model, which is created in a VS designer;
- Entity classes, which VS generates from the EDM; and
- A database to persist the EDM, which VS also generates from the EDM.
Step 2a – Create an Entity Data Model: Add an Entity Data Model (EDM) to the Common project. The EDM is added from the Add New Item dialog:
The EDM can be given whatever name is appropriate for its role. The screenshots show the EDM for the demo app included with Part 3 of this series.
The EDM will appear in the Solution Explorer as an edmx file:
Step 2b – Add entities to the EDM: Open the EDMX file in VS 2010 and a blank page will appear in the workspace pane. Open the toolbox pane and you will see tools for creating entities and relationships. Use the tools to create entities, add properties, and define associations:
The properties for the objects added to the EDM are edited in the Properties pane in the lower right corner of the VS window.
When configuring entities, keep in mind the following:
- Scalar properties are non-nullable by default. Set the
Nullable
property to true
if a property should be nullable.
- Set a default value if a property is to be non-nullable.
- To configure an entity to hold a BLOB, set the entity data type to binary. We will modify the database mappings to map to an image type below.
Step 3: Generate a Database from the EDM
Once we have created an EDM, the next step is to generate a database from the EDM. As of March 2011, VS 2010 support for this step is incomplete. Specifically, the Generate Database Wizard does not support SQL Compact 4.0, although the VS 2010 Server Explorer does. As a result, I have developed a workaround for SQL Compact 4.0 databases. The workaround can be replaced once VS 2010 provides full support for SQL Compact 4.0.
Step 3a – Create a dummy database: Once the EDM has been created, right click on the whitespace in the workspace pane and select Generate Database from Model... The Generate Database Wizard will appear. This wizard creates a SQL Compact database and a DDL script that can be run against the database to configure it for use with the EDM.
Since the Generate Database Wizard does not support SQL Compact 4.0, we will use the wizard to create a dummy SQL Compact 3.5 database that we will then discard. This step is necessary to get the wizard to generate the DDL script that we will use to configure the actual SQL Compact 4.0 database. The generated script will actually be a SQL Compact 3.5 script, but it should run fine on a SQL Compact 4.0 data file.
So, use the Generate Database Wizard to create a dummy SQL Compact 3.5 database. The name and location don’t matter; I generally create the database on the Windows desktop and use the default name for the database. Don’t execute the DDL script yet—we need to make some modifications before we use it to configure the actual database.
When the wizard completes, a new EDMX.SQLCE file will appear in the VS 2010 Solution Explorer:
We will use this file to configure our new database in a later step.
Step 3b – Modify the EDMX file: The Generate Database Wizard embeds two references to SQL Compact in the Entity Data Model (the EDMX file), because we used the wizard to generate a SQL Compact 3.5 database. We will need to open the model in its native XML format to change these references.
To do that, right-click on the EDMX file in the Solution Explorer, select Open With, and select XML (Text) Editor in the dialog that appears. The model file will open in the VS 2010 workspace as an XML file. It will look like this:
Note the two references to “3.5” in the Schema
line in the SSDL Content section. Change these references to “4.0”.
Step 3c – Modify mappings for BLOB objects: Running the Generate Database Wizard also adds mapping data to the SSDL section of the EDMX file. If you had looked at the EDMX file in the XML editor prior to running the wizard, you wouldn’t have found any mapping info in the SSDL Content section. Now that the Generate Database Wizard has been run, mapping data has been added to that section of the file. We need to modify the mappings for any BLOB objects before we go any further.
An EDM binary property normally maps to a SQL Compact varbinary
type. However, the varbinary
type has an 8K character max, which renders it unsuitable for storing BLOB objects. So, if the EDM has any BLOB properties, we have to manually reset the table mapping column to an image type. This has to be done before we generate a database from the EDM.
To reset the column, search the EDMX file (which should still be open in the XML editor) for ‘varbinary’:
You should get a hit in the <!-- SSDL content -->
section of the EDMX file. Manually replace the varbinary
reference with an image reference. Do this for each BLOB property. You are now ready to create a database.
Step 3d – Create the actual database: Use the VS 2010 Server Explorer to create the actual SQL Compact 4.0 database that the app will use. Create the database by right-clicking the Data Connections node and selecting Add Connection from the context menu that appears. Complete the Add Connection dialog by selecting SQL Compact as the data source and specifying the name and location of the data file that the app will use.
Step 3e – Modify the DDL script: Before we can execute the DDL script, we need to modify it to account for any BLOB properties in the EDM. Even though we changed the mappings above, the DDL script generated by the Generate Database Wizard specifies a varbinary
type for any data table columns that correspond to the EDM binary properties. So, before executing the script, we need to change these references in the script to the image type.
Open the EDMX.SQLCE file created by the Generate Database Wizard in VS 2010.
That’s pretty easy to do with VS 2010’s Find and Replace dialog:
Step 3f – Execute the DDL script: At this point, we are ready to execute the DDL script on the SQL Compact 4.0 database that the app will use. Open the EDMX.SQLCE file created by the Generate Database Wizard in VS 2010 and connect it to the SQL Compact 4.0 database by right-clicking on the SQL script, selecting Connection > Connect from the context menu that appears, and following the prompt. Then, execute the edmx.sqlce script by right-clicking on it again and selecting Execute SQL from the context menu.
The DDL script will execute, and VS 2010 will display a success message below the script:
If the script fails, or if ignorable errors are encountered while executing the script, an error message will be printed in red instead.
Step 4: Create a Persistence Layer
The next step in implementing EF 4 is to create a persistence (data access) layer for the application. The persistence layer in this article is designed to use a new EF 4 ObjectContext
for each request, in the case of a web app, and for each WPF form, in the case of a desktop app. The persistence layer uses the Repository pattern to structure data access. Most of the work is done by an abstract RepositoryBase<T>
class. Concrete classes implement and configure the base class for the particular Entity Data Model served by the application.
Step 4a – Add an IRepository interface to the app: Appendix A contains an IRepository<T>
interface that can be used without any modification. The FsObservableCollection<T>
class shown in Appendix B uses the interface for the Repository that is passed into it.
Step 4a – Add a Repository base class to the app: Appendix B contains a RepositoryBase<T>
class that can be used without modifications. Note that the RepositoryBase<T>
class generates its own EF 4 object context, which means that an object context factory class is not required. The class is discussed in more detail in Part 3.
Step 4b – Create Repository classes for entities: Appendix C shows a sample concrete repository class that derives from the RepositoryBase<T>
class in Appendix B. The class is discussed in Part 3 of this series.
Note that in its constructor call to the RepositoryBase<T>
constructor, BookRepository
specifies the type of the object context to be created by the repository and the name of the Entity Data Model to be used by the repository. In addition, a concrete repository may also implement any specialized data access methods not found in RepositoryBase<T>
.
Step 5: Build a View Model
Assuming that the application is designed around the MVVM pattern, the next step is to build one or more View Models for the application. The View Model should be completely isolated from any knowledge of Entity Framework or SQL Compact—one of the roles of the Persistence Layer is to provide that isolation. Appendix D contains a ViewModelBase
class that implements the INotifyPropertyChanged
interface required for WPF data binding. It also implements the INotifyPropertyChanging
interface, which provides pre-change notification of property changes. The class is discussed in Part 3 of this series.
View Model collection properties are generally of type ObservableCollection<T>
. That class has a constructor that accepts an IEnumerable<T>
and uses it to populate an observable collection. Unfortunately, additions to and deletions from an observable collection are not automatically propagated to the database. Appendix E contains a collection class, FsObservableCollection<T>
, that addresses this problem. This class takes an IRepository<T>
as an argument, in addition to the IEnumerable<T>
argument, and it uses the repository to automatically sync the collection to the database.
The FsObservableCollection<T>
class is designed to be used in MVVM View Models, instead of the regular ObservableCollection<T>
, for any entity properties that need to be persisted to the database. The class is discussed in more detail in Part 3 of this series.
Step 6: Add Commands and Services
The app will need commands and services to implement its use-cases. Generally speaking, commands are invoked by View Model command properties, and services are invoked by commands, by View Model methods, and by non-command properties.
A command contains the primary code required to execute a particular use case. To keep the command from becoming bloated, it may delegate tasks to a service class. Any code that is used by more than one command should be moved to a service class, and lengthy or complex code that is used by a single command should be moved to a service class. Any complex code that is invoked by more than one method in a single or multiple View Models should similarly be moved to service classes. The View Model should be kept as clean and tidy as possible—it is a coordinator, not a controller.
The implementation of commands and services will vary by application, so I won’t attempt a step-by-step analysis. As a general rule, I try to minimize the number of private methods in my command objects and View Models—I prefer to delegate to service classes.
Conclusion
The final part of this series will present an end-to end demo that shows how to integrate EF4 into a WPF application designed around the MVVM pattern. As always, I welcome your comments and suggestions for improving this series. I find that the peer review provided by CodeProject readers is invaluable, and it is always appreciated.
Appendix A: The IRepository Interface
The following interface specifies the contract for a Repository class for Entity Framework 4 entities. The Repository is discussed in Part 3 of this series.
using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
namespace MsDesktopStackDemo.Persistence.Interfaces
{
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);
}
}
Appendix B: The RepositoryBase<T> Class
The following class can be used as a base class for Entity Framework 4 repositories. It is discussed in Part 3 of this series. The repository allows for either an owned or a shared object context. An owned object context is disposed when the repository is disposed.
Note that the first constructor (owned object context) takes three arguments: a path to the file to be opened, the type of the object context to be built, and the name of the Entity Data Model that the class serves. The file path is provided by the calling code, and the remaining arguments are provided by a concrete repository derived from this class, in a base()
call in its constructor. See the BookRepository
sample concrete class in Appendix C.
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
{
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
#region Fields
private ObjectContext m_ObjectContext;
private IObjectSet<T> m_ObjectSet;
private bool m_UsingSharedObjectContext;
#endregion
#region Constructors
protected RepositoryBase(string filePath, Type contextType, string edmName)
{
m_ObjectContext = this.CreateObjectContext(filePath, contextType, edmName);
m_ObjectSet = m_ObjectContext.CreateObjectSet<T>();
m_UsingSharedObjectContext = false;
}
protected RepositoryBase(ObjectContext objectContext)
{
m_ObjectContext = objectContext;
m_ObjectSet = m_ObjectContext.CreateObjectSet<T>();
m_UsingSharedObjectContext = true;
}
#endregion
#region Public Methods
public void Add(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
m_ObjectSet.AddObject(entity);
}
public void Attach(T entity)
{
m_ObjectSet.Attach(entity);
}
public IQueryable<T> Fetch()
{
return m_ObjectSet;
}
public void Delete(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
m_ObjectSet.DeleteObject(entity);
}
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);
}
}
public void Dispose()
{
var disposeOfObjectContext = (m_UsingSharedObjectContext == false);
Dispose(disposeOfObjectContext);
GC.SuppressFinalize(this);
}
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
{
return m_ObjectSet.Where(predicate);
}
public T First(Expression<Func<T, bool>> predicate)
{
return m_ObjectSet.First(predicate);
}
public IEnumerable<T> GetAll()
{
return Fetch().AsEnumerable();
}
public void SaveChanges()
{
m_ObjectContext.SaveChanges();
}
public void SaveChanges(SaveOptions options)
{
m_ObjectContext.SaveChanges(options);
}
public T Single(Expression<Func<T, bool>> predicate)
{
return m_ObjectSet.Single(predicate);
}
#endregion
#region Protected Methods
protected virtual void Dispose(bool disposing)
{
if (!disposing) return;
if (m_ObjectContext == null) return;
m_ObjectContext.Dispose();
m_ObjectContext = null;
}
#endregion
#region Private Methods
private ObjectContext CreateObjectContext(string filePath, Type contextType,
string edmName)
{
if (edmName == null)
{
throw new ArgumentException("Argument 'edmName' passed in was null.");
}
if (filePath == null)
{
throw new ArgumentException("Argument 'filePath' passed in was null.");
}
var sqlCompactConnectionString = string.Format("Data Source={0}", filePath);
var builder = new EntityConnectionStringBuilder();
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();
var edmConnection = new EntityConnection(edmConnectionString);
var context = Activator.CreateInstance(contextType, edmConnection);
return (ObjectContext)context;
}
#endregion
}
}
Appendix C: A Sample Concrete Repository
The following class is an example of a concrete implementation of the base class shown in Appendix B. It is discussed in Part 3 of this series.
This class relies on the base class to do all its work. Note that the constructor takes a single argument, a file path to the data file to be opened. It passes this argument to the base class via a base()
call, along with the type of the data context to be created, and the name of the Entity Data Model served by the repository. Note that the latter two arguments are hard-coded into the concrete repository declarations and do not require any knowledge of EF4 on the part of the calling code.
using MsDesktopStackDemo.Persistence.BaseClasses;
using MsDesktopStackDemo.Model;
namespace MsDesktopStackDemo.Persistence
{
public class BookRepository : RepositoryBase<Book>
{
#region Fields
private static Type m_ContextType = typeof(BooksContainer);
private static string m_EdmName = "Model.Books";
#endregion
#region Constructor
public BookRepository(string filePath) : base(filePath, m_ContextType, m_EdmName)
{
}
#endregion
}
}
Appendix D: The ViewModelBase Class
This appendix contains a base class for an MVVM View Model. It implements the INotifyPropertyChanged
interface required for WPF data binding, and it implements the INotifyPropertyChanging
interface, which provides pre-change notification of property changes. This class is discussed in Part 3 of this series.
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
public virtual bool IgnorePropertyChangeEvents { get; set; }
#endregion
#region Public Methods
public virtual void RaisePropertyChangedEvent(string propertyName)
{
if (IgnorePropertyChangeEvents) return;
if (PropertyChanged == null) return;
var e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
public virtual void RaisePropertyChangingEvent(string propertyName)
{
if (IgnorePropertyChangeEvents) return;
if (PropertyChanging == null) return;
var e = new PropertyChangingEventArgs(propertyName);
PropertyChanging(this, e);
}
#endregion
}
}
Appendix E: A Repository-Aware Observable Collection
This appendix contains a collection class that is derived from the .NET ObservableCollection<T>
class. It is discussed in Part 3 of this series. The class is designed to be used with the Repository classes contained in Appendices A and B.
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
{
public class FsObservableCollection<T> : ObservableCollection<T> where T : class
{
#region Fields
private readonly IRepository<T> m_Repository;
#endregion
#region Constructors
public FsObservableCollection(IEnumerable<T> items, IRepository<T> repository)
: base(items ?? new T[] { })
{
if (repository == null) throw new ArgumentNullException("repository");
m_Repository = repository;
}
public FsObservableCollection(IRepository<T> repository) : base()
{
m_Repository = repository;
}
#endregion
#region Events
public event CollectionChangingEventHandler<T> CollectionChanging;
#endregion
#region Protected Method Overrides
protected override void InsertItem(int index, T item)
{
var newItems = new List<T>(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
base.InsertItem(index, item);
m_Repository.Add(item);
}
protected override void RemoveItem(int index)
{
var itemToRemove = this[index];
var oldItems = new List<T>(new[] { itemToRemove });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
base.RemoveItem(index);
m_Repository.Delete(itemToRemove);
}
protected override void ClearItems()
{
var itemsToDelete = this.ToArray();
var oldItems = new List<T>(itemsToDelete);
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
base.ClearItems();
foreach (var item in itemsToDelete)
{
m_Repository.Delete(item);
}
}
protected override void SetItem(int index, T newItem)
{
var itemToReplace = this[index];
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;
base.SetItem(index, newItem);
m_Repository.Delete(itemToReplace);
m_Repository.Add(newItem);
}
#endregion
#region Public Method Overrides
public new void Add(T item)
{
var newItems = new List<T>(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
base.Add(item);
m_Repository.Add(item);
}
public new void Clear()
{
this.Clear(true);
}
public void Clear(bool clearFromDataStore)
{
var itemsToDelete = this.ToArray();
var oldItems = new List<T>(itemsToDelete);
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
base.Clear();
if (!clearFromDataStore) return;
foreach (var item in itemsToDelete)
{
m_Repository.Delete(item);
}
}
public new void Insert(int index, T item)
{
var newItems = new List<T>(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
base.Insert(index, item);
m_Repository.Add(item);
}
public void PersistToDataStore()
{
m_Repository.SaveChanges();
}
public new void Remove(T itemToRemove)
{
var oldItems = new List<T>(new[] { itemToRemove });
var cancelled = this.RaiseCollectionChangingEvent(
NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
base.Remove(itemToRemove);
m_Repository.Delete(itemToRemove);
}
#endregion
#region Private Methods
private bool RaiseCollectionChangingEvent(
NotifyCollectionChangingAction action, IList<T> oldItems,
IList<T> newItems)
{
if (CollectionChanging == null) return false;
var e = new NotifyCollectionChangingEventArgs<T>(action, oldItems, newItems);
this.CollectionChanging(this, e);
return e.Cancel;
}
#endregion
}
}