Original Module: VCLinks_01.02.00_Install.zip
Revised Module: VCLinks_02.00.00_Install.zip
This is a reality check not for the developer who's code is being examined but for myself, the "DotNetNuke Module Development Evangelist". The code examined here achieves the most important requirement. It works. The challenge for the "Evangelist" is to provide suggestions that offer improvements in time and efficiency. Just saying that "this is the recommended way" is not good enough. Learning new techniques is a costly process itself. This must be factored into the total cost. No developer can be expected to do anything that is more costly than the alternative.
The Module
The module being examined is called VC Links. It allows an administrator to define categories and add links to each category.
Module Structure
The graphic below shows the module structure:
Module Navigation
The entry point for the application is "VCLinks.ascx". It implements the Dynamically Loaded Control pattern. The query string for each link is examined and the appropriate user control is injected.
If (Request("LinkID") <> "") Then
If (IsNumeric(Request("LinkID"))) Then
strControl = "ucAddLink.ascx"
LoadUserControl()
End If
Else
If (Request("LinkDetail") <> "") Then
If (IsNumeric(Request("LinkDetail"))) Then
strControl = "ucLinkDetail.ascx"
LoadUserControl()
Else
strControl = "ucViewAll.ascx"
LoadUserControl()
End If
Else
strControl = "ucViewAll.ascx"
LoadUserControl()
End If
End If
Recommendation
The recommended navigation is covered here: NavigateURL: How to make a link the reasons are covered here: Module Navigation Options.
Reality Check
While NavigateURL is the recommended approach it does not save any code. The primary objection to the Dynamically Loaded Control pattern that is can produce some odd behavior in certain situations because dynamically loaded controls do not save viewstate. There are solutions. For example, Denis Bauer has created a Dynamic Placeholder that maintains the viewstate of dynamically loaded controls. A tutorial describing it's implementation can be found at this link.
Since the current code works and there is no code savings the recommendation is to leave the code as it is.
The Database Layer
The module performs the standard CRUD operations. To illustrate this the link administration will be examined and a new page that contains the same functionality will be created.
When a link is updated this method is run:
Private Sub UpdateLink()
Dim intCategory As Integer = ddlCategory.SelectedValue
Dim strLinkName As String = tbLinkName.Text
Dim strLinkURL As String = tbLinkURL.Text
Dim strLinkDescription As String = tbLinkDescription.Text
'Open Connection
Dim strConn As String = ConfigurationManager.AppSettings("SiteSqlServer")
Dim objConn As New SqlConnection(strConn)
Dim cmd As New SqlCommand
cmd.CommandText = "vcLinks_UpdateLink"
cmd.CommandType = CommandType.StoredProcedure
cmd.Connection = objConn
'Declare Parameter(s)
cmd.Parameters.AddWithValue("@intVCLinksID", VCLinksID)
cmd.Parameters.AddWithValue("@intCategory", intCategory)
cmd.Parameters.AddWithValue("@strLinkName", strLinkName)
cmd.Parameters.AddWithValue("@strLinkURL", strLinkURL)
cmd.Parameters.AddWithValue("@strLinkDescription", strLinkDescription)
cmd.Parameters.AddWithValue("@intUpdatedBy", Me.UserId)
cmd.Parameters.AddWithValue("@UpdatedDate", Date.Now)
objConn.Open()
'Get ID of Restaurant Just added
cmd.ExecuteNonQuery()
objConn.Close()
End Sub
This code is very succinct and tight. It simply gathers values and passes them to the stored procedure. There is nothing wrong with this code. The data has to be gathered and passed to the database. This method logically seems as good as any other. This code calls this stored procedure:
ALTER PROCEDURE [dbo].[VCLinks_UpdateLink]
@intVCLinksID int,
@intCategory int,
@strLinkName varchar(100),
@strLinkURL varchar(100),
@strLinkDescription varchar(1000),
@intUpdatedBy int,
@UpdatedDate datetime
AS
UPDATE VCLinks
SET
VCLinksCategoryID = @intCategory,
LinkName = @strLinkName,
LinkURL = @strLinkURL,
LinkDescription = @strLinkDescription,
UpdatedBy = @intUpdatedBy,
UpdatedDate = @UpdatedDate
WHERE (VCLinksID = @intVCLinksID)
SET NOCOUNT ON
Again, the stored procedure is simple and straitforward. Looking at it, there is nothing "wrong" that stands out. You could not find fault because the DotNetNuke DAL is not being used because the DotNetNuke DAL is meant to be used in situations where you need to support multiple databases. For this module that is not the case. In addition the DotNetNuke DAL would require far more code.
While the ExecuteSQL method of the DAL+ can be used to eliminate the need to create the stored procedures, it will not save very much work or time because essentially the same code that is in the stored procedure still needs to be created in the ExecuteSQL method.
Recommendation
The recommendation is to use Linq to SQL to save the amount of code needed. In addition the wizards provided by Visual Studio, when you use declarative data binding to strongly typed data sources, will also save considerable time when creating the user interface.
Currently the functionality to administer the links is contained in the following page:
- ucAddLink.ascx.vb (387 lines of code)
The assumption we will make is that reducing this code will reduce development time and the amount of potential coding errors that have to be debugged. The stored procedures will be eliminated also but this is a by product of implementing the solution.
Create the Linq to SQL DAL
This tutorial provides an in-depth demonstration of using Linq to SQL: Creating a DotNetNuke Module using LINQ to SQL (VB and C#).
For this module, we will simply create a Linq to SQL class and drag the two tables the module uses onto it and then save the page. The DAL is created in less than a minute. This "Linq to SQL DAL" is a class that is automatically created that contains classes that are mapped directly to the database and provide the functionality to manipulate the database and it's data. In addition the Linq to SQL class provides change tracking.
The Linq Data Source control can be used to configure automatic Creates, Reads, Updates, and Deletes.
It allows for common record selection requirements to be configured using the wizard. For example this code:
Private Sub ReadQueryString()
If (Request("LinkID") <> "") Then
If (IsNumeric(Request("LinkID"))) Then
VCLinksID = CInt(Request.QueryString("LinkID"))
End If
Else
VCLinksID = Null.NullInteger
End If
End Sub
Can be configured using the wizard:
When the FormView is placed on the page and connected to the Linq Data Source control, the form is automatically created.
We need only to remove unneeded fields, add JavaScript validation controls and fix the formatting.
The new page to handle the same functionality:
- ucAdminLinks.ascx.vb (80 lines of code).
Most importantly it takes a lot less time to create.
Handling Business Rules
The module also contains this code to determine if a link already exists:
Protected Sub cvalLinkNameExist_ServerValidate
(ByVal source As Object, ByVal args As
System.Web.UI.WebControls.ServerValidateEventArgs)
Handles cvalLinkNameExist.ServerValidate
'Check to see if LinkName Already Exists
Dim intExists As Integer
'Open Connection
Dim strConn As String = ConfigurationManager.AppSettings("SiteSqlServer")
Dim objConn As New SqlConnection(strConn)
Dim cmd As New SqlCommand
cmd.CommandText = "vcLinks_Validate"
cmd.CommandType = CommandType.StoredProcedure
cmd.Connection = objConn
'Declare Parameter(s)
cmd.Parameters.AddWithValue("@ColumnNameValue", tbLinkName.Text)
objConn.Open()
'Get ID of Restaurant Just added
intExists = cmd.ExecuteScalar()
objConn.Close()
ReadQueryString()
If VCLinksID > 0 Then
args.IsValid = "True"
Else
If intExists > 0 Then
args.IsValid = "false"
Else
args.IsValid = "true"
End If
End If
End Sub
Again, we can use Linq to SQL to not only save a few lines of code but to also create a business rule that will be enforced no matter what UI control calls it. We simply add this partial class to the Linq to SQL class:
Private Sub InsertVCLink(ByVal instance As VCLink)
Dim results = From link In VCLinks.AsQueryable _
Where link.LinkName = instance.LinkName _
Or link.LinkURL = instance.LinkURL _
Select link
If results.Count() > 0 Then
Throw New Exception("Link Name or Url is already being used")
End If
Me.ExecuteDynamicInsert(instance)
End Sub
And this code to catch a possible exception and indicate that the insert was not performed:
Protected Sub FormView1_ItemInserted(ByVal sender As Object,
ByVal e As System.Web.UI.WebControls.FormViewInsertedEventArgs)
Handles FormView1.ItemInserted
If e.Exception Is Nothing Then
If boolAddAnother Then
Response.Redirect(Globals.NavigateURL(PortalSettings.ActiveTab.TabID,
Null.NullString, "LinkID=0"))
Else
Response.Redirect(NavigateURL(), True)
End If
Else
lblError.Text = e.Exception.Message
e.ExceptionHandled = True
e.KeepInInsertMode = True
End If
End Sub
Programming is Art
Programming is art much the same way constructing a building is art. We desire to follow standard conventions to construct safe buildings, yet there is plenty of room for interpretation. The artistic aspects of programming is one of the things that motivates many of us to program. Yet, as the discovery of perspective was revolutionary for art, technologies such as Linq are also revolutionary. However, it is up to the programmer, the artist, to determine when, where, and how to apply them.
Note: The Linq to SQL version of the module does not support the object qualifier feature and in order to run it on a DotNetNuke site, you need to install ASP.NET 3.5 on the server and modify the web.config of the DotNetNuke site:
Change: the <system.codedom> section to:
<system.codedom>
<compilers>
<compiler language="vb;vbs;visualbasic;vbscript" type="Microsoft.VisualBasic.VBCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" extension=".vb" warningLevel="4">
<providerOption name="CompilerVersion" value="v3.5"/>
<providerOption name="OptionInfer" value="true"/>
<providerOption name="WarnAsError" value="false"/>
</compiler>
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CSharp.CSharpCodeProvider,System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" warningLevel="4">
<providerOption name="CompilerVersion" value="v3.5"/>
<providerOption name="WarnAsError" value="false"/>
</compiler>
</compilers>
</system.codedom>
Change: the <assemblies> section to:
<assemblies>
<add assembly="Microsoft.VisualBasic, Version=8.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" />
<add assembly="System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" />
<add assembly="System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" />
<add assembly="System.Management, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" />
<add assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<add assembly="System.Data.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</assemblies>