Creating Sitefinity 4 Content Modules Part 2: Data Layer

Creating Sitefinity 4 Content Modules Part 2: Data Layer

Posted on July 11, 2011 0 Comments

The content you're reading is getting on in years
This post is on the older side and its content may be out of date.
Be sure to visit our blogs homepage for our latest news, updates and information.

Table of Contents

This series covering Sitefinity Content-Based Module creation is broken up into 5 parts:

Overview

In our last post "Project Setup and Hello World" we created the project for our content-based Locations Module. In this post, we'll be developing the data layer by implementing the necessary data classes as well as implementing the OpenAccess data provider using the new Fluent Mapping.

We’ll walk through creating each class and method, and the completed project up to this part is also available for download at the end of the post.

OpenAccess

If you skipped the first part of this series (Project Setup) you may still need to enhance your project for OpenAccess. This is a required step for Content-based modules. If you do not complete it you may receive an error similar to the following:

No metadata has been registered for class LocationsModule.Model.LocationItem. This usually indicates, that either this class is not declared persistent or it is declared persistent but not enhanced.

For more information on enhancing your project, refer to part 1 of this series or review this documentation for OpenAccess: Integration with OpenAccess Enhancer.

Data Model

Begin by creating the LocationItem.cs class file in the Model folder of your project. This is the persistent class used to represent the location. Since the Locations Module is Content-based, we need to inherit from the base Content class.

We also need a few attributes, namely DataContract for serializing the data in web services, and ManagerType which associates the data item with the content manager we’ll build later in this post.

Also, because we’re not yet supporting the Sitefinity Content LifeCycle in our class (this will be added in an advanced topic later), we need to explicitly disable it in our model.

Finally, to integrate with Sitefinity Url handling, we need to implement the ILocatable interface. We’ll implement the methods a bit later, but we’ll define it on the class now, as shown in the snippet below.

/// <summary>
/// Location Data Item class
/// </summary>
[DataContract(Namespace = "http://sitefinity.com/samples/locationmodule", Name = "LocationItem")]
[ManagerType("LocationsModule.Data.LocationsManager, LocationsModule")]
public class LocationItem : Content, ILocatable
{
    #region Overrides

    /// <summary>
    /// Indicate whether content lifecycle is supported or not
    /// </summary>
    public override bool SupportsContentLifecycle
    {
        get { return false; }
    }

    #endregion
}

We’ll also need to tap into Sitefinity’s Url handling by adding an additional LocationItemUrlData.cs class file to the Model folder. This class should inherit from UrlData, and the full implementation is as follows:

class LocationItemUrlData : UrlData
{
    /// <summary>
    /// Initializes a new instance of the <see cref="LocationItemUrlData" /> class.
    /// </summary>
    public LocationItemUrlData() : base() { this.Id = Guid.NewGuid(); }

    /// <summary>
    /// Gets or sets the parent product item
    /// </summary>
    /// <value>The product item</value>
    [NonSerializableProperty]
    public override IDataItem Parent
    {
        get
        {
            if (this.parent != null)
                ((IDataItem)this.parent).Provider = ((IDataItem)this).Provider;
            return this.parent;
        }
        set { this.parent = (LocationItem)value; }
    }

    private LocationItem parent;
}

Add Model Properties

We now need to define the properties that make up a Location Item, including things like the address as well as the Content itself. The Content in this case is an Lstring (localizeable string), similar to the Content property of a blog post, news item, event, etc.

This is where you would define your custom module's properties. For example, if you were defining an EmployeeItem you might have properties for Name, Title, PhoneNumber, etc.

Add the following code to your LocationItem class. Notice the way that properties are set and retrieved for the Lstring type.

 #region Location Properties

        /// <summary>
        /// Gets or sets the content.
        /// </summary>
        /// <value>
        /// The content.
        /// </value>
        [DataMember]
        public Lstring Content
        {
            get
            {
                if (this.content == null)
                    this.content = this.GetString("Content");
                return this.ApplyContentFilters(this.content);
            }
            set
            {
                this.content = value;
                this.SetString("Content", this.content);
            }
        }

        /// <summary>
        /// Location address
        /// </summary>
        [DataMember]
        public string Address
        {
            get { return this.address; }
            set { this.address = value; }
        }

        /// <summary>
        /// Location city
        /// </summary>
        [DataMember]
        public string City
        {
            get { return this.city; }
            set { this.city = value; }
        }

        /// <summary>
        /// Location region (state)
        /// </summary>
        [DataMember]
        public string Region
        {
            get { return this.region; }
            set { this.region = value; }
        }

        /// <summary>
        /// Location postal code
        /// </summary>
        [DataMember]
        public string PostalCode
        {
            get { return this.postalCode; }
            set { this.postalCode = value; }
        }

        #endregion

Of course make sure to include the backing private fields as well.

 #region Private Properties

        private Lstring content;
        private string address;
        private string city;
        private string region;
        private string postalCode;

        #endregion

Implement ILocateable

Finally, we need to complete our integration with Sitefinity Url handling by implementing the ILocateable interface. Once again JustCode code generation can help you by automatically adding the required using statements, references and method stubs.

Below is the full implementation of the interface for the LocationItem class to copy and paste into your class.

 #region ILocatable

        /// <summary>
        /// Gets or sets a value indicating whether to auto generate an unique URL.
        /// </summary>
        /// <value>
        ///     <c>true</c> if to auto generate an unique URL otherwise, <c>false</c>.
        /// </value>
        [NonSerializableProperty]
        public bool AutoGenerateUniqueUrl
        {
            get { return false; }
        }

        /// <summary>
        /// Gets a collection of URL data for this item.
        /// </summary>
        /// <value> The collection of URL data.</value>
        [NonSerializableProperty]
        public virtual IList<LocationItemUrlData> Urls
        {
            get
            {
                if (this.urls == null)
                    this.urls = new ProviderTrackedList<LocationItemUrlData>(this, "Urls");
                this.urls.SetCollectionParent(this);
                return this.urls;
            }
        }

        /// <summary>
        /// Gets a collection of URL data for this item.
        /// </summary>
        /// <value>The collection of URL data.</value>
        [NonSerializableProperty]
        IEnumerable<UrlData> ILocatable.Urls
        {
            get
            {
                return this.Urls.Cast<UrlData>();
            }
        }

        /// <summary>
        /// Clears the Urls collection for this item.
        /// </summary>
        /// <param name="excludeDefault">if set to <c>true</c> default urls will not be cleared.</param>
        public void ClearUrls(bool excludeDefault)
        {
            if (this.urls != null)
                this.urls.ClearUrls(excludeDefault);
        }

        /// <summary>
        /// Removes all urls that satisfy the condition that is checked in the predicate function.
        /// </summary>
        /// <param name="predicate">A function to test each element for a condition.</param>
        public void RemoveUrls(Func<UrlData, bool> predicate)
        {
            if (this.urls != null)
                this.urls.RemoveUrls(predicate);
        }

        #endregion

Be sure to also add the private backing field for the new Urls property:

private ProviderTrackedList<LocationItemUrlData> urls;

That's everything that we need for the Model folder. The completed class files are available in the downloadable project at the end of this post.

Now we can move on to creating our Data Provider for handling the actual data access.

Data Providers

Sitefinity follows the provider model, allowing you to implement different providers for different data stores, such as SQL Server, XML, or even flat files.

Generally, you would first implement a separate abstract LocationsDataProvider class from the base ContentDataProvider, then from that, derive a concrete implementation such as OpenAccessDataProvider.

However, since we are keeping things simple, we'll put it all together in one class. In later posts we'll revisit this and see how creating a separate abstract Data Provider class allows you to support multiple data storage providers.

For now, create a new OpenAccessLocationsDataProvider.cs class file inside the Data > OpenAccess folder, making sure to inherit form the Provider base class. Also, since this is an OpenAccess implementation, we need to include that interface, as well as a related Decorator attribute.

[ContentProviderDecorator(typeof(OpenAccessContentDecorator))]
public class OpenAccessLocationsDataProvider : ContentDataProviderBase, IOpenAccessDataProvider
{

}

Now we need to implement the abstract members of the ContentDataProviderBase class. Add the following code to your class; the summaries are pretty self explanatory, but I'll go into a bit more detail after the snippet.

 #region ContentDataProviderBase Overrides

        /// <summary>
        /// Gets a unique key for each data provider base.
        /// </summary>
        public override string RootKey
        {
            get { return "LocationsDataProvider"; }
        }

        /// <summary>
        /// Override this method in order to return the type of the Parent object of the specified content type.
        /// If the type has no parent type, return null.
        /// </summary>
        /// <param name="contentType">Type of the content.</param>
        /// <returns></returns>
        public override Type GetParentTypeFor(Type contentType)
        {
            return null;
        }

        /// <summary>
        /// Gets the actual type of the <see cref="T:Telerik.Sitefinity.GenericContent.Model.UrlData"/> implementation for the specified content type.
        /// </summary>
        /// <param name="itemType">Type of the content item.</param>
        /// <returns></returns>
        public override Type GetUrlTypeFor(Type itemType)
        {
            if (itemType == typeof(LocationItem)) return typeof(LocationItemUrlData);

            throw GetInvalidItemTypeException(itemType, this.GetKnownTypes());
        }

        /// <summary>
        /// Get a list of types served by this manager
        /// </summary>
        /// <returns></returns>
        public override Type[] GetKnownTypes()
        {
            return new[] { typeof(LocationItem) };
        }

        /// <summary>
        /// Gets the items by taxon.
        /// </summary>
        /// <param name="taxonId">The taxon id.</param>
        /// <param name="isSingleTaxon">A value indicating if it is a single taxon.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="itemType">Type of the item.</param>
        /// <param name="filterExpression">The filter expression.</param>
        /// <param name="orderExpression">The order expression.</param>
        /// <param name="skip">Items to skip.</param>
        /// <param name="take">Items to take.</param>
        /// <param name="totalCount">The total count.</param>
        /// <returns></returns>
        public override IEnumerable GetItemsByTaxon(Guid taxonId, bool isSingleTaxon, string propertyName, Type itemType, string filterExpression, string orderExpression, int skip, int take, ref int? totalCount)
        {
            if (itemType == typeof(LocationItemUrlData))
            {
                this.CurrentTaxonomyProperty = propertyName;
                var query = (IQueryable<LocationItem>)GetItems(itemType, filterExpression, string.Empty, skip, take, ref totalCount);
                if (isSingleTaxon)
                {
                    var query0 = from i in query
                                 where (Guid)i.GetValue(this.CurrentTaxonomyProperty) == taxonId
                                 select i;
                    if (!string.IsNullOrEmpty(orderExpression))
                        return query0.OrderBy(orderExpression);
                    return query0;
                }
                else
                {
                    var query1 = from i in query
                                 where ((IList)i.GetValue(this.CurrentTaxonomyProperty)).Contains(taxonId)
                                 select i;
                    if (!string.IsNullOrEmpty(orderExpression))
                        return query1.OrderBy(orderExpression);
                    return query1;
                }
            }
            throw GetInvalidItemTypeException(itemType, this.GetKnownTypes());
        }

        #endregion

RootKey simply identifies your provider. GetParentTypeFor would be used for modules with Parent-Child relationships, such as Lists > ListItems and Blogs > BlogPosts. Since we have no such relationship we return null.

GetUrlTypeFor is a utility method that simply returns the LocationItemUrlData we declared earlier. Similarly, GetKnownTypes is a list of the supported Types that your provider handles. In this case we only have the one LocationItem type.

Finally, the GetItemsByTaxon allows the provider to hook into the Sitefinity taxonomy system to retrieve related items of the LocationItem data types by category or tag. This code can be reused in other custom classes you develop, just make sure to change the LocationItem to match your module's data model class.

You can optionally override one additional method to modify the Url format used by for accessing specific content items by Url. By default it uses the dated Url format used by blog posts (yyyy/mm/dd/title).

The code below modifies it to simply use the location Url name.

/// <summary>
/// Gets the url format for the specified data item that implements <see cref="T:Telerik.Sitefinity.GenericContent.Model.ILocatable"/> interface.
/// </summary>
/// <param name="item">The locatable item for which the url format should be returned.</param>
/// <returns>
/// Regular expression used to format the url.
/// </returns>
public override string GetUrlFormat(ILocatable item)
{
    if (item.GetType() == typeof(LocationItem))
    {
        return "/[UrlName]";
    }

    return base.GetUrlFormat(item);
}

Generic Item Methods

In addition to the GetItemsByTaxon method above, modules in Sitefinity need to implement generic methods that are independent of your content type. This allows you to use your custom type in the different components of Sitefinity without having to re-implement them for your specific type.

Fortunately, these implementations are also fairly straightforward. Here is the complete code for these methods, including the previous GetItemsByTaxon method.

Note, I did not replicate this method, I simply moved it into the #region with the other methods to help organize the code.

 #region Generic Item Methods

        /// <summary>
        /// Creates new data item.
        /// </summary>
        /// <param name="itemType">Type of the item.</param>
        /// <param name="id">The pageId.</param>
        /// <returns></returns>
        public override object CreateItem(Type itemType, Guid id)
        {
            if (itemType == null)
                throw new ArgumentNullException("itemType");

            if (itemType == typeof(LocationItem))
            {
                return this.CreateLocation(id);
            }

            throw GetInvalidItemTypeException(itemType, this.GetKnownTypes());
        }

        /// <summary>
        /// Gets the data item with the specified ID.
        /// An exception should be thrown if an item with the specified ID does not exist.
        /// </summary>
        /// <param name="itemType">Type of the item.</param>
        /// <param name="id">The ID of the item to return.</param>
        /// <returns></returns>
        public override object GetItem(Type itemType, Guid id)
        {
            if (itemType == null)
                throw new ArgumentNullException("itemType");

            if (itemType == typeof(LocationItem))
                return this.GetLocation(id);

            return base.GetItem(itemType, id);
        }

        /// <summary>
        /// Gets the items.
        /// </summary>
        /// <param name="itemType">Type of the item.</param>
        /// <param name="filterExpression">The filter expression.</param>
        /// <param name="orderExpression">The order expression.</param>
        /// <param name="skip">The skip.</param>
        /// <param name="take">The take.</param>
        /// <param name="totalCount">Total count of the items that are filtered by <paramref name="filterExpression"/></param>
        /// <returns></returns>
        public override IEnumerable GetItems(Type itemType, string filterExpression, string orderExpression, int skip, int take, ref int? totalCount)
        {
            if (itemType == null)
                throw new ArgumentNullException("itemType");

            if (itemType == typeof(LocationItem))
            {
                return SetExpressions(this.GetLocations(), filterExpression, orderExpression, skip, take, ref totalCount);
            }

            if (itemType == typeof(Comment))
            {
                return SetExpressions(this.GetComments(), filterExpression, orderExpression, skip, take, ref totalCount);
            }

            throw GetInvalidItemTypeException(itemType, this.GetKnownTypes());
        }

        /// <summary>
        /// Get item by primary key without throwing exceptions if it doesn't exist
        /// </summary>
        /// <param name="itemType">Type of the item to get</param>
        /// <param name="id">Primary key</param>
        /// <returns>
        /// Item or default value
        /// </returns>
        public override object GetItemOrDefault(Type itemType, Guid id)
        {
            if (itemType == typeof(Comment))
            {
                return this.GetComments().Where(c => c.Id == id).FirstOrDefault();
            }

            if (itemType == typeof(LocationItem))
                return this.GetLocations().Where(n => n.Id == id).FirstOrDefault();

            return base.GetItemOrDefault(itemType, id);
        }

        /// <summary>
        /// Retrieve a content item by its Url, optionally returning only items that are visible on the public side
        /// </summary>
        /// <param name="itemType">Type of the item to get</param>
        /// <param name="url">Url of the item (relative)</param>
        /// <param name="published">If true, will get only Published/Scheduled items - those that are typically visible on the public side.</param>
        /// <param name="redirectUrl">Url to redirect to if the item's url has been changed</param>
        /// <returns>Data item or null</returns>
        public override IDataItem GetItemFromUrl(Type itemType, string url, bool published, out string redirectUrl)
        {
            if (itemType == null)
                throw new ArgumentNullException("itemType");
            if (String.IsNullOrEmpty(url))
                throw new ArgumentNullException("Url");

            var urlType = this.GetUrlTypeFor(itemType);

            var    urlData = this.GetUrls(urlType).Where(u => u.Url == url).FirstOrDefault();


            if (urlData != null)
            {
                var item = urlData.Parent;

                if (urlData.RedirectToDefault)
                    redirectUrl = this.GetItemUrl((ILocatable)item, CultureInfo.GetCultureInfo(urlData.Culture));
                else
                    redirectUrl = null;
                if (item != null)
                    item.Provider = this;
                return item;
            }
            redirectUrl = null;
            return null;
        }


        /// <summary>
        /// Gets the items by taxon.
        /// </summary>
        /// <param name="taxonId">The taxon id.</param>
        /// <param name="isSingleTaxon">A value indicating if it is a single taxon.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="itemType">Type of the item.</param>
        /// <param name="filterExpression">The filter expression.</param>
        /// <param name="orderExpression">The order expression.</param>
        /// <param name="skip">Items to skip.</param>
        /// <param name="take">Items to take.</param>
        /// <param name="totalCount">The total count.</param>
        /// <returns></returns>
        public override IEnumerable GetItemsByTaxon(Guid taxonId, bool isSingleTaxon, string propertyName, Type itemType, string filterExpression, string orderExpression, int skip, int take, ref int? totalCount)
        {
            if (itemType == typeof(LocationItemUrlData))
            {
                this.CurrentTaxonomyProperty = propertyName;
                var query = (IQueryable<LocationItem>)GetItems(itemType, filterExpression, string.Empty, skip, take, ref totalCount);
                if (isSingleTaxon)
                {
                    var query0 = from i in query
                                 where (Guid)i.GetValue(this.CurrentTaxonomyProperty) == taxonId
                                 select i;
                    if (!string.IsNullOrEmpty(orderExpression))
                        return query0.OrderBy(orderExpression);
                    return query0;
                }
                else
                {
                    var query1 = from i in query
                                 where ((IList)i.GetValue(this.CurrentTaxonomyProperty)).Contains(taxonId)
                                 select i;
                    if (!string.IsNullOrEmpty(orderExpression))
                        return query1.OrderBy(orderExpression);
                    return query1;
                }
            }
            throw GetInvalidItemTypeException(itemType, this.GetKnownTypes());
        }

        /// <summary>
        /// Marks the provided persistent item for deletion.
        /// The item is deleted form the storage when the transaction is committed.
        /// </summary>
        /// <param name="item">The item to be deleted.</param>
        public override void DeleteItem(object item)
        {
            this.DeleteLocation((LocationItem)item);
        }

        #endregion

CRUD Methods

Now we need to define the actual CRUD methods that the provider supports. This is where the actual interaction with the database through OpenAccess occurs.

For more details on implementing OpenAccess data methods, take a look at the documentation article “Working With Data”, or simply add the following code to your class.

 #region CRUD Methods

        /// <summary>
        /// Create a location item with random id
        /// </summary>
        /// <returns>
        /// Newly created agent item in transaction
        /// </returns>
        public LocationItem CreateLocation()
        {
            return this.CreateLocation(Guid.NewGuid());
        }

        /// <summary>
        /// Create a location item with specific primary key
        /// </summary>
        /// <param name="id">Primary key</param>
        /// <returns>
        /// Newly created agent item in transaction
        /// </returns>
        public LocationItem CreateLocation(Guid id)
        {
            var location = new LocationItem();
            location.Id = id;
            location.ApplicationName = this.ApplicationName;
            location.Owner = SecurityManager.GetCurrentUserId();
            var dateValue = DateTime.UtcNow;
            location.DateCreated = dateValue;
            location.PublicationDate = dateValue;
            ((IDataItem)location).Provider = this;

            // items with empty guid are used in the UI to get a "blank" data item
            // -> i.e. to fill a data item with default values
            // if this is the case, we leave the item out of the transaction
            if (id != Guid.Empty)
            {
                this.GetContext().Add(location);
            }
            return location;
        }

        /// <summary>
        /// Get a location item by primary key
        /// </summary>
        /// <param name="id">Primary key</param>
        /// <returns>
        /// Agent item in transaction
        /// </returns>
        /// <exception cref="T:Telerik.Sitefinity.SitefinityExceptions.ItemNotFoundException">When there is no item with the given primary key</exception>
        public LocationItem GetLocation(Guid id)
        {
            if (id == Guid.Empty)
                throw new ArgumentException("Id cannot be Empty Guid");

            // Always use this method. Do NOT change it to query. Catch the exception if the Id can be wrong.
            var location = this.GetContext().GetItemById<LocationItem>(id.ToString());
            ((IDataItem)location).Provider = this;

            return location;
        }

        public IQueryable<LocationItem> GetLocations()
        {
            var appName = this.ApplicationName;

            var query =
                SitefinityQuery
                .Get<LocationItem>(this, MethodBase.GetCurrentMethod())
                .Where(b => b.ApplicationName == appName);

            return query;
        }

        public void DeleteLocation(LocationItem location)
        {
            var scope = this.GetContext();
            if (scope != null)
            {
                scope.Remove(location);
            }
        }

        #endregion

OpenAccess Fluent Mapping

Sitefinity Q1 (4.1) now includes the latest OpenAccess Q1 2011, which includes support for Fluent Mapping, eliminating the need for complex XML files or designer wizards to setup your database mappings.

Additional discussion of using OpenAccess in Sitefinity modules is available in the previous articles “Important Considerations before upgrading to Sitefinity 4.1”, andChanges in Mapping Persistent Classes in Custom Modules with Sitefinity 4.1”, as well as a series of videos walking you through using the new Fluent Mapping. Of course there is also extensive documentation about fluent mapping with OpenAccess from Telerik.

The basic idea is to use the fluent mapping to map your Data Model classes (in our module that is LocationItem and LocationItemUrlData and their relationships to database tables.

Create a new LocationsFluentMapping.cs file inside the Data > OpenAccess folder. The following code handles setting up these mappings.

public class LocationsFluentMapping : OpenAccessFluentMappingBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="LocationsFluentMapping"/> class.
    /// </summary>
    /// <param name="context">The context.</param>
    public LocationsFluentMapping(IDatabaseMappingContext context) : base(context) { }

    /// <summary>
    /// Creates and returns a collection of OpenAccess mappings
    /// </summary>
    public override IList<MappingConfiguration> GetMapping()
    {
        // initialize and return mappings
        var mappings = new List<MappingConfiguration>();
        MapItem(mappings);
        MapUrlData(mappings);
        return mappings;
    }

    /// <summary>
    /// Maps the LocationItem class.
    /// </summary>
    /// <param name="mappings">The LocationItem class mappings.</param>
    private void MapItem(IList<MappingConfiguration> mappings)
    {
        // initialize mapping
        var itemMapping = new MappingConfiguration<LocationItem>();
        itemMapping.HasProperty(p => p.Id).IsIdentity();
        itemMapping.MapType(p => new { }).ToTable("sf_locations");

        // add properties
        itemMapping.HasProperty(p => p.Address);
        itemMapping.HasProperty(p => p.City);
        itemMapping.HasProperty(p => p.Region);
        itemMapping.HasProperty(p => p.PostalCode);

        // map urls table association
        itemMapping.HasAssociation(p => p.Urls).WithOppositeMember("parent", "Parent").ToColumn("content_id").IsDependent().IsManaged();
        mappings.Add(itemMapping);
    }

    /// <summary>
    /// Maps the LocationItemUrlData class
    /// </summary>
    /// <param name="mappings">The LocatoinItemUrlData class mappings.</param>
    private void MapUrlData(IList<MappingConfiguration> mappings)
    {
        // map the urldata type
        var urlDataMapping = new MappingConfiguration<LocationItemUrlData>();
        urlDataMapping.MapType(p => new { }).Inheritance(InheritanceStrategy.Flat).ToTable("sf_url_data");
        mappings.Add(urlDataMapping);
    }
}

These mappings need to be injected into the OpenAccess context using a MetaDataSource. Create a new LocationsFluentMetaDataSource.cs class file in the Data > OpenAccess folder and define it with the complete implementation below.

public class LocationsFluentMetadataSource : ContentBaseMetadataSource
{
    public LocationsFluentMetadataSource() : base(null) { }

    /// <summary>
    /// Initializes a new instance of the <see cref="LocationsFluentMetadataSource"/> class.
    /// </summary>
    /// <param name="context">The context.</param>
    public LocationsFluentMetadataSource(IDatabaseMappingContext context) : base(context) { }

    /// <summary>
    /// Builds the custom mappings for the data provider.
    /// </summary>
    /// <returns></returns>
    protected override IList<IOpenAccessFluentMapping> BuildCustomMappings()
    {
        var sitefinityMappings = base.BuildCustomMappings();
        sitefinityMappings.Add(new LocationsFluentMapping(this.Context));
        return sitefinityMappings;
    }
}

Finally, we need to retrieve this MetaDataSource from our provider. This is done by implementing the IOpenAccessDataProvider interface. Add the following code to your class. Note that the Context property only needs to be defined; no further implementation is needed.

 #region IOpenAccessDataProvider

        /// <summary>
        /// Gets or sets the OpenAccess context. Alternative to Database.
        /// </summary>
        /// <value>
        /// The context.
        /// </value>
        public OpenAccessProviderContext Context { get; set; }

        /// <summary>
        /// Gets the meta data source.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public MetadataSource GetMetaDataSource(Telerik.Sitefinity.Model.IDatabaseMappingContext context)
        {
            return new LocationsFluentMetadataSource(context);
        }

        #endregion

That's everything you need for the OpenAccess data provider. Now we move on to the final piece of our Data Model.

Content Manager

Every Content-based module in Sitefinity requires an associated Manager used to simplify accessing data. Blogs use the BlogsManager, News uses the NewsManager, and so on.

Our module is no different; we'll be defining a LocationsManager. However, we need to first implement a dependent class.

Module Configuration

Because Sitefinity uses the Provider Model, Content Managers are unaware of the specific implementation you will be using. They need to be aware of what providers are available, and this is done by reading a configuration file for the module.

These files are located in the ~/App_Data/Sitefinity/Configuration folder, matching the name of the module. For example, Blogs uses blogConfig.config and so on. We need to create our own configuration file for our module.

Fortunately, Sitefinity makes this very easy. All we need to do is inherit from the ContentModuleConfigBase and override a few methods. Create a new LocationsConfig.cs class file inside the Configuration folder and define it with the following code.

class LocationsConfig : ContentModuleConfigBase
{
    /// <summary>
    /// Initializes the default providers.
    /// </summary>
    /// <param name="providers">The providers.</param>
    protected override void InitializeDefaultProviders(ConfigElementDictionary<string, DataProviderSettings> providers)
    {
        // add default provider
        providers.Add(new DataProviderSettings(providers)
        {
            Name = "OpenAccessLocationsDataProvider",
            Description = "A provider that stores locations data in database using OpenAccess ORM.",
            ProviderType = typeof(OpenAccessLocationsDataProvider),
            Parameters = new NameValueCollection() { { "applicationName", "/Locations" } }
        });
    }

    protected override void InitializeDefaultViews(ConfigElementDictionary<string, ContentViewControlElement> contentViewControls)
    {
        // TODO: initialize views after we define them!
    }

    /// <summary>
    /// Gets or sets the name of the default data provider that is used to manage security.
    /// </summary>
    [ConfigurationProperty("defaultProvider", DefaultValue = "OpenAccessLocationsDataProvider")]
    public override string DefaultProvider
    {
        get { return (string)this["defaultProvider"]; }
        set { this["defaultProvider"] = value; }
    }
}

Here we are simply adding the provider we just defined to the configuration, then overriding the DefaultProvider with attributes so that it is assigned as the default provider for the module. The remaining InitializeDefaultViews is used to initialize the frontend and backend administration views, and will be implemented after we create them in the next post.

Locations Manager

Now that we've defined our configuration, create a new LocationsManager.cs class file inside the Data folder. This needs to inherit from the ContentManagerBase using the DataProvider class we defined earlier.

Also, even though we are not supporting Content Lifecycle in our module, the web service class we’ll be defining later for the backend controls requires that our module implements this interface.

This interface requires two sets of related methods, one for the concrete LocationItem class, and another for the generic Content item. Since we are not implementing Content Lifecycle, we just need to define the methods, but do not need to actually implement them.

Here is a snip of the code up to this point. The complete class definition is available here as well as in the downloadable project at the end of this post.

public class LocationsManager : ContentManagerBase<OpenAccessLocationsDataProvider>, IContentLifecycleManager<LocationItem>
{

    #region IContentLifecycleManager

        #region Location Item methods

        public LocationItem CheckIn(LocationItem item)
        {
            throw new NotImplementedException();
        }

        // ... additional LocationItem interface methods

        #endregion

        #region Generic methods

        public Content CheckIn(Content item)
        {
            throw new NotImplementedException();
        }

        // ... additional generic interface methods

        #endregion

    #endregion
}

We also need to implement constructors for the class so it can be correctly instantiated when used by other Sitefinity classes. Add the following code to your class.

 #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="LocationsManager"/> class.
        /// </summary>
        public LocationsManager() : this(null) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="LocationsManager"/> class.
        /// </summary>
        /// <param name="providerName">Name of the provider.</param>
        public LocationsManager(string providerName) : base(providerName) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="LocationsManager"/> class.
        /// </summary>
        /// <param name="providerName">Name of the provider.</param>
        /// <param name="transactionName">Name of the transaction.</param>
        public LocationsManager(string providerName, string transactionName) : base(providerName, transactionName) { }

        #endregion

Similarly, we need a set of static GetManager methods that simplify retrieval of the LocationsManager. This allows us to simply call LocationsManager.GetManager() to instantiate the class when using it in code.

Add the following code to your class.

 #region GetManager Methods

        /// <summary>
        /// Gets the default manager.
        /// </summary>
        /// <returns></returns>
        public static LocationsManager GetManager() { return ManagerBase<OpenAccessLocationsDataProvider>.GetManager<LocationsManager>(); }

        /// <summary>
        /// Gets the default manager.
        /// </summary>
        /// <param name="providerName">Name of the provider.</param>
        /// <returns></returns>
        public static LocationsManager GetManager(string providerName) { return ManagerBase<OpenAccessLocationsDataProvider>.GetManager<LocationsManager>(providerName); }


        /// <summary>
        /// Gets the manager.
        /// </summary>
        /// <param name="providerName">Name of the provider.</param>
        /// <param name="transactionName">Name of the transaction.</param>
        /// <returns></returns>
        public static LocationsManager GetManager(string providerName, string transactionName) { return ManagerBase<OpenAccessLocationsDataProvider>.GetManager<LocationsManager>(providerName, transactionName); }

        #endregion

Next we define the most important methods in our class, namely the CRUD methods that call the underlying data methods to retrieve content data. These are the main methods we'll be using to access content for our module.

Add the following code to your module and note how all we're doing here is wiring up the manager to the data provider.

 #region CRUD Methods

        /// <summary>
        /// Creates a new location item.
        /// </summary>
        /// <returns></returns>
        public LocationItem CreateLocation() { return this.Provider.CreateLocation(); }

        /// <summary>
        /// Creates a new location item with the specified id.
        /// </summary>
        /// <param name="id">The location item id.</param>
        /// <param name="applicationName">Name of the application.</param>
        /// <returns></returns>
        public LocationItem CreateLocation(Guid id) { return this.Provider.CreateLocation(id); }

        /// <summary>
        /// Gets the location item with the specified id.
        /// </summary>
        /// <param name="id">The location item id.</param>
        /// <returns></returns>
        public LocationItem GetLocation(Guid id) { return this.Provider.GetLocation(id); }

        /// <summary>
        /// Gets the full list of location items.
        /// </summary>
        /// <returns></returns>
        public IQueryable<LocationItem> GetLocations() { return this.Provider.GetLocations(); }

        /// <summary>
        /// Deletes the specified location item.
        /// </summary>
        /// <param name="location">The location item.</param>
        public void DeleteLocation(LocationItem location) { this.Provider.DeleteLocation(location); }

        /// <summary>
        /// Deletes location item with the specified id.
        /// </summary>
        /// <param name="id">The id.</param>
        public void DeleteLocation(Guid id) { this.Provider.DeleteLocation(this.Provider.GetLocation(id)); }

        #endregion

It is here that you can define extended public methods to extend the basic methods of the data provider. For example, the data provider can only delete using the LocationItem, but our Manager allows us to also delete by the content ID.

At this point, all that is left is to implement the abstract methods from the ContentManagerBase class. Add this final code snippet to your class.

 #region ContentManagerBase Overrides

        /// <summary>
        /// Gets the name of the module.
        /// </summary>
        /// <value>
        /// The name of the module.
        /// </value>
        public override string ModuleName
        {
            get { return LocationsModule.ModuleName; }
        }

        /// <summary>
        /// Gets the providers settings.
        /// </summary>
        protected override ConfigElementDictionary<string, DataProviderSettings> ProvidersSettings
        {
            get { return Config.Get<LocationsConfig>().Providers; }
        }

        /// <summary>
        /// Gets the default provider delegate.
        /// </summary>
        protected override GetDefaultProvider DefaultProviderDelegate
        {
            get { return () => Config.Get<LocationsConfig>().DefaultProvider; }
        }

        /// <summary>
        /// Get items by type
        /// </summary>
        /// <typeparam name="TItem">The type of the item.</typeparam>
        /// <returns>IQueryable</returns>
        public override IQueryable<TItem> GetItems<TItem>()
        {
            if (typeof(LocationItem).IsAssignableFrom(typeof(TItem)))
                return this.GetLocations() as IQueryable<TItem>;
            if (typeof(TItem) == typeof(UrlData) || typeof(TItem) == typeof(LocationItemUrlData))
                return this.GetUrls<LocationItemUrlData>() as IQueryable<TItem>;
            throw new NotSupportedException();
        }

        #endregion

Notice how we're using the static ModuleName property from our LocationsModule class. We also retrieve the providers from the config file using the LocationsConfig class.

The DefaultProviderDelegate class is a little different. It returns a delegate method for retrieving the default data provider. There are several ways to implement this, but the simplest is to just define the delegate inline that returns the default provider from the LocationsConfig class.

Finally, a generic GetItems class is required that returns LocationItems or LocationItemUrlData items. With this, the Data Layer of our Locations Module is now complete.

Up Next

Now that we have a data layer in place, we need to create both the administrative backend as well as the frontend widgets that will allow our end-users to use the module. This will be the focus of our next post, continuing from the solution which is available for download below.

Downloads

progress-logo

The Progress Team

View all posts from The Progress Team on the Progress blog. Connect with us about all things application development and deployment, data integration and digital business.

Comments

Comments are disabled in preview mode.
Topics

Sitefinity Training and Certification Now Available.

Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.

Learn More
Latest Stories
in Your Inbox

Subscribe to get all the news, info and tutorials you need to build better business apps and sites

Loading animation