DotNetNuke 5 module development tutorial - part 3: The data layer

5. February 2010 17:06

To Visual Basic version

In this part we are going to take a look at how the data layer is organized, set up the database objects and create the functionality needed to access the data.

Of course you are free to access your data any way you want. You might not even have the need to access the database. Anyway, the project template provides us with a data layer structure that I found quite okay to work with.
Your module controls call methods in the ProductsController class (our BLL), which calls methods in the SqlDataProvider class (part of our DAL, which inherits from the abstract DataProvider class). SqlDataProvider then access the database by calling stored procedures (also part of our DAL). The stored procedure calls are made using the Microsoft Enterprise Library.

So, where do we start? Let's build from the ground up and start with the SQL scripts that set up your database objects.
Open the file 01.00.00.SqlDataProvider. As you can see the file is really just an SQL batch script with placeholders for database owner and object prefix ({databaseOwner} and {objectQualifier}). On module installation the placeholders are replaced with the current DotNetNuke installation's values for database owner and object prefix.
I suggest we begin with a clean slate, so delete everything in the file. Insert the following instead:

/** Create Table **/
if not exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_Products]') and OBJECTPROPERTY(id, N'IsTable') = 1)
    BEGIN
        CREATE TABLE {databaseOwner}[{objectQualifier}SipidCode_Products]
        (
            [ModuleID] [int] NOT NULL,
            [ItemID] [int] NOT NULL IDENTITY(1, 1),
            [Name] [nvarchar](200) NOT NULL,
            [Description] [nvarchar](2000) NOT NULL
        )

        ALTER TABLE {databaseOwner}[{objectQualifier}SipidCode_Products] ADD CONSTRAINT [PK_{objectQualifier}SipidCode_Products] PRIMARY KEY NONCLUSTERED  ([ItemID])
        CREATE CLUSTERED INDEX [IX_{objectQualifier}SipidCode_Products] ON {databaseOwner}[{objectQualifier}SipidCode_Products] ([ModuleID])

        ALTER TABLE {databaseOwner}[{objectQualifier}SipidCode_Products] WITH NOCHECK ADD CONSTRAINT [FK_{objectQualifier}SipidCode_Products_{objectQualifier}Modules] FOREIGN KEY ([ModuleID]) REFERENCES {databaseOwner}[{objectQualifier}Modules] ([ModuleID]) ON DELETE CASCADE NOT FOR REPLICATION
    END
GO

To not loose focus we keep the data structure fairly simple... nothing too weird here. The ModuleID column is used to associate the product items to a specific module instance. The rest of the columns belong to the actual product item.
Now that we have a table we need some stored procedures to access its data. But before we create the stored procedures it's a good idea to drop them if the already exist. So add the following:

/** Drop Existing Stored Procedures **/

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_GetProducts]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_GetProducts
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_GetProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_GetProduct
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_AddProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_AddProduct
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_UpdateProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_UpdateProduct
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_DeleteProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_DeleteProduct
GO

And lastly, creation of the stored procedures:

/** Create Stored Procedures **/

create procedure {databaseOwner}{objectQualifier}SipidCode_GetProducts
    @ModuleId int
as

select ModuleId,
       ItemId,
       [Name],
       Description
from {objectQualifier}SipidCode_Products
where  ModuleId = @ModuleId
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_GetProduct
     @ModuleId int,
     @ItemId    int
as

select ModuleId,
       ItemId,
       [Name],
       Description
from {objectQualifier}SipidCode_Products
where  ModuleId = @ModuleId
and ItemId = @ItemId
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_AddProduct
    @ModuleId    int,
    @Name         nvarchar(200),
    @Description  nvarchar(2000)
as

insert into {objectQualifier}SipidCode_Products (
       ModuleId,
       [Name],
       Description
)
values (
    @ModuleId,
    @Name,
    @Description
)
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_UpdateProduct
    @ModuleId     int,
    @ItemId        int,
    @Name          nvarchar(200),
    @Description   nvarchar(2000)
as

update {objectQualifier}SipidCode_Products
set    Name       = @Name,
        Description = @Description
where ModuleId = @ModuleId
and ItemId = @ItemId
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_DeleteProduct
    @ModuleId       int,
    @ItemId          int
as

delete
from   {objectQualifier}SipidCode_Products
where  ModuleId = @ModuleId
and    ItemId = @ItemId
GO

To store the data from the database and use it the application code, we use a class as data holder. This class only members are public properties corresponding to the database table columns we are interested in. Open the ProductsInfo.cs file in the Components folder.
As you see there is just an empty constructor and some old-style public properties. Delete all class members and make the class look like this:

public class ProductsInfo
{
    public int ModuleId { get; set; }
    public int ItemId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

Clear and concise. We love auto-implemented properties, don't we?
With the data objects prepaired we need to get our data provider in the data access layer in order.
Open the file DataProvider.cs in the Components folder. The interesting stuff is found in the "Abstract methods" region. We want to remove the last "s" from the method names and adjust the parameters of AddProduct and UpdateProduct so they look like this:

public abstract IDataReader GetProducts(int ModuleId);
public abstract IDataReader GetProduct(int ModuleId, int ItemId);
public abstract void AddProduct(int ModuleId, string Name, string Description);
public abstract void UpdateProduct(int ModuleId, int ItemId, string Name, string Description);
public abstract void DeleteProduct(int ModuleId, int ItemId);

Now open SqlDataProvider.cs so we can make matching changes there. Expand the "Public Methods" region. We need to change these methods so they match the abstract methods in DataProvider. Besides changing the method signatures we also have to adjust the implementations. The string parameter in the calls to GetFullyQualifiedName() should match the name of the stored procedure we want to call. We also have to change the parameters sent to the stored procedures. The methods in the "Public Methods" region should look like this:

public override IDataReader GetProducts(int ModuleId)
{
    return (IDataReader)SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetProducts"), ModuleId);
}

public override IDataReader GetProduct(int ModuleId, int ItemId)
{
    return (IDataReader)SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetProduct"), ModuleId, ItemId);
}

public override void AddProduct(int ModuleId, string Name, string Description)
{
    SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("AddProduct"), ModuleId, Name, Description);
}

public override void UpdateProduct(int ModuleId, int ItemId, string Name, string Description)
{
    SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("UpdateProduct"), ModuleId, ItemId, Name, Description);
}

public override void DeleteProduct(int ModuleId, int ItemId)
{
    SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("DeleteProduct"), ModuleId, ItemId);
}

Then we need to make matching changes in the ProductsController class. As I said before ProductsController calls the methods in SqlDataProvider to get the data and returns it to the controls. In our simple implementation there is not very much interesting going on in ProductsController. In a more advanced scenario there could be business specific logic in there, thereby earning ProductsController the name "business logic layer".
We want the methods in the "Public methods" region in ProductsController.cs to look like this:

public List<ProductsInfo> GetProducts(int ModuleId)
{
    return CBO.FillCollection<ProductsInfo>(DataProvider.Instance().GetProducts(ModuleId));
}

public ProductsInfo GetProduct(int ModuleId, int ItemId)
{
    return (ProductsInfo)CBO.FillObject(DataProvider.Instance().GetProduct(ModuleId, ItemId), typeof(ProductsInfo));
}

public void AddProduct(ProductsInfo prod)
{
    if (prod.Name.Trim() != "")
    {
        DataProvider.Instance().AddProduct(prod.ModuleId, prod.Name, prod.Description);
    }
}

public void UpdateProduct(ProductsInfo prod)
{
    if (prod.Name.Trim() != "")
    {
        DataProvider.Instance().UpdateProduct(prod.ModuleId, prod.ItemId, prod.Name, prod.Description);
    }
}

public void DeleteProduct(int ModuleId, int ItemId)
{
    DataProvider.Instance().DeleteProduct(ModuleId, ItemId);
}

 

Remove the inheritance from interfaces ISearchable and IPortable, changing the class declaration from

public class ProductsController : ISearchable, IPortable

to

public class ProductsController

Delete the methods GetSearchItems, ExportModule and ImportModule. We will revisit these in later posts.

With that done we now have the functionality we need to access the database and get/set/update our data. Next time we will put it to use in the control for the module's edit view.

Tags: , ,

DotNetNuke | Modules | Tutorials

Comments

2/22/2010 11:30:10 AM #

Amrita


As i am following all the steps as discussed in this tutorial but while i am installing the module i am getting this error..please guide me for this

"DotNetNuke.Services.Exceptions.ModuleLoadException: An entry with the same key already exists. ---> System.ArgumentException: An entry with the same key already exists. at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource) at System.Collections.Generic.SortedList`2.Add(TKey key, TValue value) at DotNetNuke.Entities.Modules.DesktopModuleController.GetPortalDesktopModules(Int32 portalID) at DotNetNuke.UI.ControlPanels.IconBar.BindData() in E:\Wisethink\dotnetnuke\DotNetNuke_Professional_TRIAL_Install\Admin\ControlPanel\IconBar.ascx.vb:line 60 at DotNetNuke.UI.ControlPanels.IconBar.Page_Load(Object sender, EventArgs e) in E:\Wisethink\dotnetnuke\DotNetNuke_Professional_TRIAL_Install\Admin\ControlPanel\IconBar.ascx.vb:line 326 --- End of inner exception stack trace ---"

Amrita India |

2/22/2010 10:50:05 PM #

Johan

Amrita,
I have not been able to reproduce the exception you are experiencing.
Is it at the end of part 5 of this tutorial it occurs?

It looks like you are trying to add a module control with an already existing key. But that you get an exception instead of an error message inside DotNetNuke seems strange.

Johan Sweden |

4/15/2010 11:38:38 PM #

Herb Orejel

Substantially, the article is really the greatest on this deserving topic. I fit in with your conclusions and will thirstily look forward to your upcoming updates. Just saying thanks will not just be sufficient, for the wonderful lucidity in your writing. I will directly grab your rss feed to stay abreast of any updates. Good work and much success in your business dealings!

Herb Orejel United States |

4/18/2010 11:02:03 PM #

trackback

DotNetNuke 5 module development tutorial - part 4: The view control

DotNetNuke 5 module development tutorial - part 4: The view control

Sipid Code |

1/5/2011 11:38:03 PM #

Lars

Is the SQL batch script in 01.00.00.SqlDataProvider supposed to genereat e new table that i can see in as a table in the database via SQL server management studio?

Lars Denmark |

1/6/2011 2:04:43 PM #

Johan

Yep.

Johan Sweden |

1/6/2011 3:25:38 PM #

Lars

Strange - I have a simple hello world module that does nothing but displaying an input and a button. I added the first bit of create table script from above. Build and uploaded the module with no errors and even a bit saying executing sql. But no new tables appeared in DB.... Jätte kunstigt Smile

Is anything else needed to invoke the script.

Lars Denmark |

1/7/2011 10:38:06 AM #

Johan

Did you follow the tutorial to the end and created an installation package which you ran, or did you run the SQL script manually from inside DNN?

Johan Sweden |

1/19/2011 11:29:17 PM #

Lars

Finally deleted the hello-world (without sql script) module i allready had installed and then installed then new package. And. . . . there was the new database table. Is this due to version number on then file.

Lars Denmark |

1/20/2011 8:44:26 PM #

Johan

The version number in the script file name should not make any difference in this case. Are you sure you didn't make any other changes?

Johan Sweden |

1/20/2011 10:13:07 PM #

Lars

Well - i am sort of creating my own hello world and not exactly following your very nice tutorial from a to z, but - no i didn't change anything else. I just uninstalled the "old" module from dnn before i installed the new package and then it worked. I'm sure I will figure out the rest as i go allong - thx for a nice tutorial...

Lars Denmark |

4/12/2011 2:52:19 PM #

Legomonster

Great tutorials! Very useful

Legomonster Ireland |

12/20/2011 8:07:51 AM #

Deepak

Hi there,

  Thanks for such easy and to the point dotnetnuke installations steps. I followed your steps but somehow I am not able to run my project. Its giving error (The type or namespace name 'DotNetNuke' could not be found (are you missing a using directive or an assembly reference?). I tried changing the framework version to 3.5 and 4.0 but still no luck.

Can you point out what I am missing?

Thanks
Deepak

Deepak India |

3/1/2012 10:52:38 PM #

Simcha

I have the same problem. ANYONE?

Simcha United States |

5/11/2012 5:02:23 AM #

pingback

Pingback from duyanhpham.wordpress.com

HÆ°á»›ng dẫn viết module DNN 5 – DotNetNuke 5 module development tutorial  « Phạm Duy Anh

duyanhpham.wordpress.com |

9/26/2013 8:26:04 AM #

thilip

how to add the module to the page

thilip France |

3/8/2014 11:48:33 AM #

trackback

DotNetNuke 5 module development tutorial - part 7: Creating the installation package

DotNetNuke 5 module development tutorial - part 7: Creating the installation package

Sipid Code |

Comments are closed

About the addict

Johan Seppäläinen lives in Uppsala, Sweden. He spends most of his days working as a systems architect/developer, specialized in solutions built on Microsoft platforms.
Occasionally there is time for some recreational coding, when he pursues optimal solutions and code zen, mainly in C#. When he is not writing in this blog, that is.