Click here to Skip to main content
15,884,353 members
Articles / Desktop Programming / WPF

Using the Microsoft Desktop Stack – Part 2: Using Entity Framework 4.0 with SQL Compact 4.0

Rate me:
Please Sign up or sign in to vote.
4.58/5 (9 votes)
12 Apr 2011CPOL15 min read 36.7K   30   1
This series of articles explains how to use the Microsoft Stack and presents checklists for implementing it. This part explains using Entity Framework 4.0 with SQL Compact 4.0.

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:

Image 1

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:

Image 2

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:

Image 3

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:

Image 4

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:

Image 5

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’:

Image 6

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:

Image 7

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:

Image 8

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.

C#
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);
    }
}

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.

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
{
    /// <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
    }
}

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.

C#
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
    }
}

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.

C#
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
     }
}

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.

C#
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
    }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Foresight Systems
United States United States
David Veeneman is a financial planner and software developer. He is the author of "The Fortune in Your Future" (McGraw-Hill 1998). His company, Foresight Systems, develops planning and financial software.

Comments and Discussions

 
SuggestionRepositoryBase<T> Class Pin
OrlandoCurioso18-Sep-11 4:46
OrlandoCurioso18-Sep-11 4:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.